From ef83e349e3c4d055f8fc96f9fa2e3ffdcddee616 Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 12 Jul 2024 06:31:20 -0700 Subject: [PATCH 1/4] fix: Make PaseEncoder Sendable (#175) * fix: Make PaseEncoder Sendable * revert back to internal * Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- Sources/ParseSwift/Coding/ParseEncoder.swift | 2 +- Sources/ParseSwift/ParseConstants.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52d7c673..e665e9e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,14 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.11.0 +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.0/documentation/parseswift) + +__Fixes__ +* Make ParseEncoder sendable ([#175](https://github.com/netreconlab/Parse-Swift/pull/175)), thanks to [Corey Baker](https://github.com/cbaker6). ### 5.10.0 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.9.3...5.10.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.0/documentation/parseswift) diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 38cb94b46..ed1d0b69c 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -54,7 +54,7 @@ extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, ParseEncoder matches the features of the [Swift 5.4 JSONEncoder ](https://github.com/apple/swift/blob/main/stdlib/public/Darwin/Foundation/JSONEncoder.swift). Update commits as needed for improvement. */ -public struct ParseEncoder { +public struct ParseEncoder: Sendable { let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? let outputFormatting: JSONEncoder.OutputFormatting? diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 2742007c6..3c4398f19 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "5.10.0" + static let version = "5.10.1" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" From b3e84a205565266112321c4141338d68719bbcbe Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 13 Jul 2024 08:16:15 -0700 Subject: [PATCH 2/4] docs: Fix latest version in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e665e9e41..ab03652b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ -### 5.11.0 +### 5.10.1 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.0/documentation/parseswift) __Fixes__ From 173e7d8d339e9d5f4288c54dc63a088b2062a0d0 Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 13 Jul 2024 11:56:17 -0700 Subject: [PATCH 3/4] fix: Improve ParseObject conformance to Hashable (#176) * fix: Improve ParseObject conformance to Hashable * nit * another nit * Test ParseError hashing * Use compiler level equatable for ParseFile * fix file test * improve identifiable documentation --- CHANGELOG.md | 10 +++- Sources/ParseSwift/Objects/ParseObject.swift | 25 ++++------ Sources/ParseSwift/Objects/ParseRole.swift | 7 --- Sources/ParseSwift/ParseConstants.swift | 2 +- Sources/ParseSwift/Protocols/Fileable.swift | 8 --- .../Protocols/ParseHookParametable.swift | 2 +- .../ParseSwift/Protocols/ParseTypeable.swift | 2 +- Sources/ParseSwift/Types/ParseError.swift | 11 ++++ Sources/ParseSwift/Types/ParseFile.swift | 50 +++++++++---------- .../ParseSwift/Types/ParseHookResponse.swift | 2 +- Sources/ParseSwift/Types/Query.swift | 2 +- Tests/ParseSwiftTests/ParseErrorTests.swift | 17 +++++++ .../ParseObjectAsyncTests.swift | 2 +- 13 files changed, 76 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab03652b2..be188c0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.10.2 +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...5.10.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.2/documentation/parseswift) + +__Fixes__ +* Improve ParseObject conformance to Hashable to prevent collision attacks ([#176](https://github.com/netreconlab/Parse-Swift/pull/176)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 5.10.1 -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.0/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.0...5.10.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.1/documentation/parseswift) __Fixes__ * Make ParseEncoder sendable ([#175](https://github.com/netreconlab/Parse-Swift/pull/175)), thanks to [Corey Baker](https://github.com/cbaker6). diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index ccb4e3973..9659993b0 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -172,16 +172,6 @@ public protocol ParseObject: ParseTypeable, // MARK: Default Implementations public extension ParseObject { - /** - A computed property that is a unique identifier and makes it easy to use `ParseObject`'s - as models in MVVM and SwiftUI. - - note: `id` allows `ParseObject`'s to be used even if they have not been saved and/or missing an `objectId`. - - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. - */ - var id: String { - objectId ?? UUID().uuidString - } - var mergeable: Self { guard isSaved, originalData == nil else { @@ -245,13 +235,18 @@ extension ParseObject { } } -// MARK: Hashable +// MARK: Identifiable public extension ParseObject { - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(createdAt) - hasher.combine(updatedAt) + + /** + A computed property that ensures `ParseObject`'s can be uniquely identified across instances. + - note: `id` allows `ParseObject`'s to be uniquely identified even if they have not been saved and/or missing an `objectId`. + - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. + */ + var id: String { + objectId ?? UUID().uuidString } + } // MARK: Helper Methods diff --git a/Sources/ParseSwift/Objects/ParseRole.swift b/Sources/ParseSwift/Objects/ParseRole.swift index fd91a62af..4cc771164 100644 --- a/Sources/ParseSwift/Objects/ParseRole.swift +++ b/Sources/ParseSwift/Objects/ParseRole.swift @@ -104,13 +104,6 @@ public extension ParseRole { self.ACL = acl } - func hash(into hasher: inout Hasher) { - let name = self.name ?? self.objectId - hasher.combine(name) - hasher.combine(createdAt) - hasher.combine(updatedAt) - } - func mergeParse(with object: Self) throws -> Self { guard hasSameObjectId(as: object) else { throw ParseError(code: .otherCause, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 3c4398f19..a26bdf7da 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "5.10.1" + static let version = "5.10.2" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Protocols/Fileable.swift b/Sources/ParseSwift/Protocols/Fileable.swift index 156fdb3d8..4a7f6e49e 100644 --- a/Sources/ParseSwift/Protocols/Fileable.swift +++ b/Sources/ParseSwift/Protocols/Fileable.swift @@ -18,12 +18,4 @@ extension Fileable { var isSaved: Bool { return url != nil } - - public static func == (lhs: Self, rhs: Self) -> Bool { - guard let lURL = lhs.url, - let rURL = rhs.url else { - return lhs.id == rhs.id - } - return lURL == rURL - } } diff --git a/Sources/ParseSwift/Protocols/ParseHookParametable.swift b/Sources/ParseSwift/Protocols/ParseHookParametable.swift index 4bb8c146b..65fa8d725 100644 --- a/Sources/ParseSwift/Protocols/ParseHookParametable.swift +++ b/Sources/ParseSwift/Protocols/ParseHookParametable.swift @@ -12,4 +12,4 @@ import Foundation Conforming to `ParseHookParametable` allows types that can be created to decode parameters in `ParseHookFunctionRequest`'s. */ -public protocol ParseHookParametable: Codable, Equatable, Sendable {} +public protocol ParseHookParametable: ParseTypeable {} diff --git a/Sources/ParseSwift/Protocols/ParseTypeable.swift b/Sources/ParseSwift/Protocols/ParseTypeable.swift index 31e3df094..2bf8bf6a9 100644 --- a/Sources/ParseSwift/Protocols/ParseTypeable.swift +++ b/Sources/ParseSwift/Protocols/ParseTypeable.swift @@ -13,7 +13,7 @@ import Foundation */ public protocol ParseTypeable: Codable, Sendable, - Equatable, + Hashable, CustomDebugStringConvertible, CustomStringConvertible {} diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index 5478f9725..ad7c6e728 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -540,6 +540,17 @@ extension ParseError: LocalizedError { } } +// MARK: Hashable +extension ParseError { + public func hash(into hasher: inout Hasher) { + hasher.combine(code) + hasher.combine(message) + hasher.combine(error) + hasher.combine(otherCode) + hasher.combine(swift?.localizedDescription) + } +} + // MARK: Equatable extension ParseError: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index c33011109..bc9bb8bb6 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -22,28 +22,6 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable { && data == nil } - /** - A computed property that is a unique identifier and makes it easy to use `ParseFile`'s - as models in MVVM and SwiftUI. - - note: `id` allows `ParseFile`'s to be used even when they are not saved. - - important: `id` will have the same value as `name` when a `ParseFile` is saved. - */ - public var id: String { - guard isSaved else { - guard let cloudURL = cloudURL else { - guard let localURL = localURL else { - guard let data = data else { - return name - } - return "\(name)_\(data)" - } - return combineName(with: localURL) - } - return combineName(with: cloudURL) - } - return name - } - /** The name of the file. Before the file is saved, this is the filename given by the user. @@ -159,10 +137,6 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable { self.options = options } - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } - public func isSaved() async throws -> Bool { isSaved } @@ -174,6 +148,30 @@ public struct ParseFile: Fileable, Savable, Deletable, Hashable, Identifiable { } } +// MARK: Identifiable +extension ParseFile { + /** + A computed property that ensures `ParseFile`'s can be uniquely identified across instances. + - note: `id` allows `ParseFile`'s to be uniquely identified even if they have not been saved. + - important: `id` will have the same value as `objectId` when a `ParseObject` contains an `objectId`. + */ + public var id: String { + guard isSaved else { + guard let cloudURL = cloudURL else { + guard let localURL = localURL else { + guard let data = data else { + return name + } + return "\(name)_\(data)" + } + return combineName(with: localURL) + } + return combineName(with: cloudURL) + } + return name + } +} + // MARK: Helper Methods (internal) extension ParseFile { func combineName(with url: URL) -> String { diff --git a/Sources/ParseSwift/Types/ParseHookResponse.swift b/Sources/ParseSwift/Types/ParseHookResponse.swift index 8a9e52167..5a81377b5 100644 --- a/Sources/ParseSwift/Types/ParseHookResponse.swift +++ b/Sources/ParseSwift/Types/ParseHookResponse.swift @@ -12,7 +12,7 @@ import Foundation Build a response after processing a `ParseHookFunctionRequest` or `ParseHookTriggerRequest`. */ -public struct ParseHookResponse: ParseTypeable { +public struct ParseHookResponse: ParseTypeable { /// The data to return in the response. public var success: R? /// An object with a Parse code and message. diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 677fc870c..336bce03f 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -132,7 +132,7 @@ public struct Query: ParseTypeable where T: ParseObject { - parameter key: The key to order by. */ - public enum Order: Codable, Equatable, Sendable { + public enum Order: ParseTypeable { /// Sort in ascending order based on `key`. case ascending(String) /// Sort in descending order based on `key`. diff --git a/Tests/ParseSwiftTests/ParseErrorTests.swift b/Tests/ParseSwiftTests/ParseErrorTests.swift index 71b5b5fe9..ce163bd6b 100644 --- a/Tests/ParseSwiftTests/ParseErrorTests.swift +++ b/Tests/ParseSwiftTests/ParseErrorTests.swift @@ -159,6 +159,23 @@ class ParseErrorTests: XCTestCase { XCTAssertNil(error.containedIn([.operationForbidden, .invalidQuery])) } + func testHashing() throws { + let error1 = ParseError(code: .accountAlreadyLinked, message: "Hello") + let error2 = ParseError(code: .accountAlreadyLinked, message: "World") + let error3 = error1 + + var setOfSameErrors = Set([error1, error1, error3]) + XCTAssertEqual(setOfSameErrors.count, 1) + XCTAssertEqual(setOfSameErrors.first, error1) + XCTAssertEqual(setOfSameErrors.first, error3) + XCTAssertNotEqual(setOfSameErrors.first, error2) + setOfSameErrors.insert(error2) + XCTAssertEqual(setOfSameErrors.count, 2) + XCTAssertTrue(setOfSameErrors.contains(error1)) + XCTAssertTrue(setOfSameErrors.contains(error2)) + XCTAssertTrue(setOfSameErrors.contains(error3)) + } + func testErrorCount() throws { let errorCodes = ParseError.Code.allCases XCTAssertGreaterThan(errorCodes.count, 50) diff --git a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift index d56a2647d..5469b6c5f 100644 --- a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift @@ -1801,7 +1801,7 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(savedGame.objectId, gameOnServer.objectId) XCTAssertEqual(savedGame.createdAt, gameOnServer.createdAt) XCTAssertEqual(savedGame.updatedAt, gameOnServer.createdAt) - XCTAssertEqual(savedGame.profilePicture, gameOnServer.profilePicture) + XCTAssertEqual(savedGame.profilePicture?.url, gameOnServer.profilePicture?.url) } #endif } From f13ad6b7ecee396e6fecb2685d3e171823ff6c3d Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 13 Jul 2024 15:54:04 -0700 Subject: [PATCH 4/4] fix: Encode id and className properties for nested non-ParseObjects (#177) --- CHANGELOG.md | 8 ++++- Sources/ParseSwift/Coding/ParseEncoder.swift | 19 +++++++----- Sources/ParseSwift/ParseConstants.swift | 2 +- .../ParseEncoderExtraTests.swift | 6 ++++ Tests/ParseSwiftTests/ParseObjectTests.swift | 29 +++++++++++++++++++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be188c0e5..7805f94ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.3...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.10.3 +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.2...5.10.3), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.3/documentation/parseswift) + +__Fixes__ +* Allow encoding of id and className on nested types that are not ParseObjects ([#176](https://github.com/netreconlab/Parse-Swift/pull/177)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 5.10.2 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.10.1...5.10.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.10.2/documentation/parseswift) diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index ed1d0b69c..96f949116 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -72,14 +72,17 @@ public struct ParseEncoder: Sendable { case custom(Set) func keys() -> Set { - let defaultObjectKeys = Set(["createdAt", - "updatedAt", - "objectId", - "className", - "emailVerified", - "id", - "score", - "originalData"]) + let defaultObjectKeys = Set( + [ + "objectId", + "createdAt", + "updatedAt", + "emailVerified", + "score", + "originalData" + ] + ) + switch self { case .object: diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index a26bdf7da..f0d915e82 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "5.10.2" + static let version = "5.10.3" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift b/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift index 64dcf8b9f..75782c26c 100644 --- a/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift +++ b/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift @@ -10,6 +10,11 @@ import XCTest @testable import ParseSwift class ParseEncoderTests: XCTestCase { + + struct Dummy: Codable, Hashable { + var id: String + } + struct GameScore: ParseObject, ParseQueryScorable { // These are required by ParseObject var objectId: String? @@ -24,6 +29,7 @@ class ParseEncoderTests: XCTestCase { // Your own properties var points: Int + var dummy: Dummy? // a custom initializer init() { diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 7cc648181..6cf6a0e2e 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -11,6 +11,11 @@ import XCTest @testable import ParseSwift class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Dummy: Codable, Hashable { + var id: String + } + struct Level: ParseObject { var objectId: String? @@ -43,6 +48,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var level: Level? var levels: [Level]? var nextLevel: Level? + var dummy: Dummy? //: custom initializers init() {} @@ -348,6 +354,29 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertTrue(score1.shouldRestoreKey(\.nextLevel, original: score2)) } + func testParseEncoderAllowsIdOnNestedTypesOnParseObject() throws { + var score = GameScore(points: 5) + score.dummy = Dummy(id: "hello") + + let object = try ParseCoding + .parseEncoder() + .encode( + score, + acl: nil, + collectChildren: true, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil + ) + let decoded = String( + decoding: object.encoded, + as: UTF8.self + ) + XCTAssertEqual( + decoded, + #"{"dummy":{"id":"hello"},"player":"Jen","points":5}"# + ) + } + func testParseObjectMutable() throws { var score = GameScore(points: 19, name: "fire") XCTAssertEqual(score, score.mergeable)