Skip to content

Commit

Permalink
Allow generic return types for ParseCloud, hint, and explain (#92)
Browse files Browse the repository at this point in the history
* Allow generic return types for ParseCloud, hint, and explain

* Change encoder/decoder of in memory storage

* Make all test cases throw on setup and teardown

* nits, improve decoding error reporting

* nits and improved error reporting

* Update changelog

* Improve decode error

* Improve decode error reporting and add test case

* tweak keychain tests

* Remove unnecessary error check

* lower codecov patch

* Prepare 1.2.0 release

* Add date decoding test

* nits
  • Loading branch information
cbaker6 authored Mar 15, 2021
1 parent 0b0d775 commit 82cbecd
Show file tree
Hide file tree
Showing 43 changed files with 380 additions and 433 deletions.
2 changes: 1 addition & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ coverage:
status:
patch:
default:
target: auto
target: 72
changes: false
project:
default:
Expand Down
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# Parse-Swift Changelog

### main
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.7...main)
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

### 1.1.7
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...1.1.7)
### 1.2.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...1.2.0)

__Breaking changes__
- Allows return types to be specified for `ParseCloud`, query `hint`, and `explain` (see playgrounds for examples). Changed functionality of synchronous `query.first()`. It use to return nil if no values are found. Now it will throw an error if none are found. ([#92](https://github.com/parse-community/Parse-Swift/pull/92)), thanks to [Corey Baker](https://github.com/cbaker6).

__New features__
- Add transaction support to batch saveAll and deleteAll ([#89](https://github.com/parse-community/Parse-Swift/pull/89)), thanks to [Corey Baker](https://github.com/cbaker6).
- Add modifiers to containsString, hasPrefix, hasSuffix ([#85](https://github.com/parse-community/Parse-Swift/pull/85)), thanks to [Corey Baker](https://github.com/cbaker6).

__Improvements__
- Better error reporting when decode errors occur ([#92](https://github.com/parse-community/Parse-Swift/pull/92)), thanks to [Corey Baker](https://github.com/cbaker6).
- Can use a variadic version of exclude. Added examples of select and exclude query in playgrounds ([#88](https://github.com/parse-community/Parse-Swift/pull/88)), thanks to [Corey Baker](https://github.com/cbaker6).

### 1.1.6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ initializeParse()

//: Create your own value typed `ParseCloud` type.
struct Cloud: ParseCloud {

//: Return type of your Cloud Function
typealias ReturnType = String

//: These are required for Object
var functionJobName: String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct GameScore: ParseObject {
var ACL: ParseACL?
var location: ParseGeoPoint?
//: Your own properties
var score: Int
var score: Int?

//: A custom initializer.
init(score: Int) {
Expand Down Expand Up @@ -126,7 +126,6 @@ query3.find { results in
switch results {
case .success(let scores):

assert(scores.count >= 1)
scores.forEach { (score) in
print("""
Someone has a score of \"\(score.score)\" with no geopoint \(String(describing: score.location))
Expand Down Expand Up @@ -164,7 +163,6 @@ query7.find { results in
switch results {
case .success(let scores):

assert(scores.count >= 1)
scores.forEach { (score) in
print("""
Someone has a score of \"\(score.score)\" with geopoint using OR \(String(describing: score.location))
Expand All @@ -177,11 +175,19 @@ query7.find { results in
}

//: Explain the previous query.
let explain = try query2.find(explain: true)
let explain: AnyDecodable = try query2.first(explain: true)
print(explain)

let hint = try query2.find(explain: false, hint: "objectId")
print(hint)
//: Hint of the previous query (asynchronous)
query2.find(explain: false,
hint: "_id_") { (result: Result<[GameScore], ParseError>) in
switch result {
case .success(let scores):
print(scores)
case .failure(let error):
print(error.localizedDescription)
}
}

PlaygroundPage.current.finishExecution()
//: [Next](@next)
2 changes: 1 addition & 1 deletion ParseSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "ParseSwift"
s.version = "1.1.7"
s.version = "1.2.0"
s.summary = "Parse Pure Swift SDK"
s.homepage = "https://github.com/parse-community/Parse-Swift"
s.authors = {
Expand Down
2 changes: 1 addition & 1 deletion Scripts/jazzy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ bundle exec jazzy \
--author_url http://parseplatform.org \
--github_url https://github.com/parse-community/Parse-Swift \
--root-url http://parseplatform.org/Parse-Swift/api/ \
--module-version 1.1.7 \
--module-version 1.2.0 \
--theme fullwidth \
--skip-undocumented \
--output ./docs/api \
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/API/API+Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ internal extension API {
var urlRequest = URLRequest(url: urlComponents)
urlRequest.allHTTPHeaderFields = headers
if let urlBody = body {
if (urlBody as? ParseCloud) != nil {
if (urlBody as? CloudType) != nil {
guard let bodyData = try? ParseCoding.parseEncoder().encode(urlBody, skipKeys: .cloud) else {
return .failure(ParseError(code: .unknownError,
message: "couldn't encode body \(urlBody)"))
Expand Down
8 changes: 4 additions & 4 deletions Sources/ParseSwift/API/Responses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ internal struct FileUploadResponse: Decodable {
}

// MARK: AnyResultResponse
internal struct AnyResultResponse: Codable {
let result: AnyCodable?
internal struct AnyResultResponse<U: Decodable>: Decodable {
let result: U
}

// MARK: AnyResultsResponse
internal struct AnyResultsResponse: Codable {
let results: AnyCodable?
internal struct AnyResultsResponse<U: Decodable>: Decodable {
let results: [U]
}

// MARK: ConfigResponse
Expand Down
10 changes: 9 additions & 1 deletion Sources/ParseSwift/API/URLSession+extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ extension URLSession {
return .failure(error)
}
guard let parseError = error as? ParseError else {
guard JSONSerialization.isValidJSONObject(responseData) == true,
let json = try? JSONSerialization
.data(withJSONObject: responseData,
options: .prettyPrinted) else {
return .failure(ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription) Format: \(String(describing: String(data: responseData, encoding: .utf8)))"))
}
return .failure(ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription)"))
message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription) Format: \(String(describing: String(data: json, encoding: .utf8)))"))
}
return .failure(parseError)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Coding/ParseCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension ParseCoding {
message: "An invalid date string was provided when decoding dates."
)
}
} catch let error {
} catch {
let container = try decoder.container(keyedBy: DateEncodingKeys.self)

if
Expand Down
11 changes: 6 additions & 5 deletions Sources/ParseSwift/Objects/ParseInstallation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,13 +395,14 @@ extension ParseInstallation {
completion(result)
}
}
} catch let error as ParseError {
callbackQueue.async {
completion(.failure(error))
}
} catch {
callbackQueue.async {
completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription)))
if let error = error as? ParseError {
completion(.failure(error))
} else {
completion(.failure(ParseError(code: .unknownError,
message: error.localizedDescription)))
}
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions Sources/ParseSwift/Objects/ParseObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -539,13 +539,14 @@ extension ParseObject {
completion(result)
}
}
} catch let error as ParseError {
callbackQueue.async {
completion(.failure(error))
}
} catch {
callbackQueue.async {
completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription)))
if let error = error as? ParseError {
completion(.failure(error))
} else {
completion(.failure(ParseError(code: .unknownError,
message: error.localizedDescription)))
}
}
}
}
Expand Down Expand Up @@ -646,7 +647,7 @@ extension ParseObject {
}

internal func saveCommand() -> API.Command<Self, Self> {
return API.Command<Self, Self>.saveCommand(self)
API.Command<Self, Self>.saveCommand(self)
}

// swiftlint:disable:next function_body_length
Expand Down
11 changes: 6 additions & 5 deletions Sources/ParseSwift/Objects/ParseUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -706,13 +706,14 @@ extension ParseUser {
}
}
}
} catch let error as ParseError {
callbackQueue.async {
completion(.failure(error))
}
} catch {
callbackQueue.async {
completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription)))
if let error = error as? ParseError {
completion(.failure(error))
} else {
completion(.failure(ParseError(code: .unknownError,
message: error.localizedDescription)))
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

enum ParseConstants {
static let parseVersion = "1.1.7"
static let parseVersion = "1.2.0"
static let hashingKey = "parseSwift"
static let fileManagementDirectory = "parse/"
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Protocols/Queryable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public protocol Queryable {
associatedtype ResultType

func find(options: API.Options) throws -> [ResultType]
func first(options: API.Options) throws -> ResultType?
func first(options: API.Options) throws -> ResultType
func count(options: API.Options) throws -> Int
func find(options: API.Options, callbackQueue: DispatchQueue,
completion: @escaping (Result<[ResultType], ParseError>) -> Void)
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Storage/ParseKeyValueStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public protocol ParseKeyValueStore {
/// It works by encoding / decoding all values just like a real `Codable` store would
/// but it stores all values as `Data` blobs in memory.
struct InMemoryKeyValueStore: ParseKeyValueStore {
var decoder = JSONDecoder()
var encoder = JSONEncoder()
var decoder = ParseCoding.jsonDecoder()
var encoder = ParseCoding.jsonEncoder()
var storage = [String: Data]()

mutating func delete(valueFor key: String) throws {
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Types/ParseCloud+combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public extension ParseCloud {
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func runFunctionPublisher(options: API.Options = []) -> Future<AnyCodable, ParseError> {
func runFunctionPublisher(options: API.Options = []) -> Future<ReturnType, ParseError> {
Future { promise in
self.runFunction(options: options,
completion: promise)
Expand All @@ -37,7 +37,7 @@ public extension ParseCloud {
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func startJobPublisher(options: API.Options = []) -> Future<AnyCodable, ParseError> {
func startJobPublisher(options: API.Options = []) -> Future<ReturnType, ParseError> {
Future { promise in
self.startJob(options: options,
completion: promise)
Expand Down
50 changes: 21 additions & 29 deletions Sources/ParseSwift/Types/ParseCloud.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@

import Foundation

public protocol CloudType: Decodable, CustomDebugStringConvertible { }

/**
Objects that conform to the `ParseCloud` protocol are able to call Parse Cloud Functions and Jobs.
An object should be instantiated for each function and job type. When conforming to
`ParseCloud`, any properties added will be passed as parameters to your Cloud Function or Job.
*/
public protocol ParseCloud: ParseType, Decodable, CustomDebugStringConvertible {
public protocol ParseCloud: ParseType, CloudType {

associatedtype ReturnType: Decodable
/**
The name of the function or job.
*/
Expand All @@ -27,10 +31,10 @@ extension ParseCloud {
/**
Calls a Cloud Code function *synchronously* and returns a result of it's execution.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: Returns a JSON response of `AnyCodable` type.
- returns: Returns a `Decodable` type.
- throws: An error of type `ParseError`.
*/
public func runFunction(options: API.Options = []) throws -> AnyCodable {
public func runFunction(options: API.Options = []) throws -> ReturnType {
try runFunctionCommand().execute(options: options, callbackQueue: .main)
}

Expand All @@ -39,11 +43,11 @@ extension ParseCloud {
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
- parameter completion: A block that will be called when logging out, completes or fails.
It should have the following argument signature: `(Result<AnyCodable, ParseError>)`.
It should have the following argument signature: `(Result<ReturnType, ParseError>)`.
*/
public func runFunction(options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AnyCodable, ParseError>) -> Void) {
completion: @escaping (Result<ReturnType, ParseError>) -> Void) {
runFunctionCommand()
.executeAsync(options: options, callbackQueue: callbackQueue) { result in
callbackQueue.async {
Expand All @@ -52,19 +56,13 @@ extension ParseCloud {
}
}

internal func runFunctionCommand() -> API.Command<Self, AnyCodable> {
internal func runFunctionCommand() -> API.Command<Self, ReturnType> {

return API.Command(method: .POST,
path: .functions(name: functionJobName),
body: self) { (data) -> AnyCodable in
let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse.self, from: data)
guard let result = response.result else {
if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) {
throw error
}
return AnyCodable()
}
return result
body: self) { (data) -> ReturnType in
let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse<ReturnType>.self, from: data)
return response.result
}
}
}
Expand All @@ -74,9 +72,9 @@ extension ParseCloud {
/**
Starts a Cloud Code job *synchronously* and returns a result with the jobStatusId of the job.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: Returns a JSON response of `AnyCodable` type.
- returns: Returns a `Decodable` type.
*/
public func startJob(options: API.Options = []) throws -> AnyCodable {
public func startJob(options: API.Options = []) throws -> ReturnType {
try startJobCommand().execute(options: options, callbackQueue: .main)
}

Expand All @@ -85,11 +83,11 @@ extension ParseCloud {
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
- parameter completion: A block that will be called when logging out, completes or fails.
It should have the following argument signature: `(Result<AnyCodable, ParseError>)`.
It should have the following argument signature: `(Result<ReturnType, ParseError>)`.
*/
public func startJob(options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AnyCodable, ParseError>) -> Void) {
completion: @escaping (Result<ReturnType, ParseError>) -> Void) {
startJobCommand()
.executeAsync(options: options, callbackQueue: callbackQueue) { result in
callbackQueue.async {
Expand All @@ -98,18 +96,12 @@ extension ParseCloud {
}
}

internal func startJobCommand() -> API.Command<Self, AnyCodable> {
internal func startJobCommand() -> API.Command<Self, ReturnType> {
return API.Command(method: .POST,
path: .jobs(name: functionJobName),
body: self) { (data) -> AnyCodable in
let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse.self, from: data)
guard let result = response.result else {
if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) {
throw error
}
return AnyCodable()
}
return result
body: self) { (data) -> ReturnType in
let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse<ReturnType>.self, from: data)
return response.result
}
}
}
Expand Down
Loading

0 comments on commit 82cbecd

Please sign in to comment.