Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Result ALL THE THINGS!!! #644

Merged
merged 21 commits into from
Jul 22, 2019
Merged

Result ALL THE THINGS!!! #644

merged 21 commits into from
Jul 22, 2019

Conversation

designatednerd
Copy link
Contributor

@designatednerd designatednerd commented Jul 17, 2019

This PR changes pretty much all of our completion closures to take advantage of Swift 5's built-in Result type. This is going to be a SUPER-BREAKING CHANGE™, though I believe it will help significantly in disambiguating whether a particular request has succeeded or failed, which is why I've done it in the first place.

If you have reasons you think I should abandon this path, I'd love to hear them - please let me know in the comments!

In this PR:

  • Changed basically anything which had the following signature:
    completionHandler: ((Object?, Error?) -> Void)
    to use this signature instead:
    completionHandler: ((Result<Object, Error>) -> Void)
    This takes advantage of Swift's built in Result type to make it WAY easier to tell when a request has failed prior to parsing. This does cause a couple of significant changes:
    • Previously, there were cases where we would return neither a result nor an error. These cases will now always return a result.

    • This leads to some extra-fun and inscrutable compiler errors when you're going through and updating everything:

      Screen Shot 2019-07-16 at 6 25 28 PM

      What this error is attemping to tell you is that you need to change the two-parameter completion closure into a single-parameter one. It's telling you that in a very, very opaque fashion, though. I plan to document this in the release notes as well.

  • Updated all tests - I'll flag the ones which are significant changes in comments.
  • Removed deprecated method OperationResultHandler typealias since I'm already breaking the crap out of everything

} else {
XCTFail("Unexpected error: \(error)")
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test represents a breaking change: Before if you cleared the cache and requested the same info again, you would get nil for both the GraphQLResult and the Error. Now you will receive an error since there is nothing to decode for that result.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That definitely seems correct to me, I think the previous behavior was a mistake.

let websocketError = WebSocketError(payload: payload,
error: error,
kind: .neitherErrorNorPayloadReceived)
responseHandler(.failure(websocketError))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new error type to help handle cases where we previously could have sent nil for both the payload and the error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the full context here (this was contributed by someone else), but I'm wondering when this condition would actually occur. I suspect this is something that should be solved in OperationMessage.parse, because it seems like a protocol error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I plan to look at that in more depth when I do some much more detailed looks at the WebSocket stuff- this is at least to get this out the door in a way that still gives us some kind of semi-meaningful error about what went wrong.

} else {
XCTFail("Unexpected error: \(error)")
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test also represents a breaking change - I think that this has an outer GraphQLResultError since there's some data in the cache, but not all of it.

@designatednerd designatednerd added this to the 0.13.0 milestone Jul 17, 2019
@designatednerd
Copy link
Contributor Author

Twitter feedback on this change has been positive so far - gonna let @martijnwalraven tell me what I'm doing wrong tomorrow and then leave it open for further feedback for a day or two.

@designatednerd designatednerd added enhancement Issues outlining new things we want to do or things that will make our lives as devs easier swift-5 Issues specific to Swift 5.0 labels Jul 17, 2019

@available(*, deprecated, renamed: "GraphQLResultHandler")
public typealias OperationResultHandler<Operation: GraphQLOperation> = GraphQLResultHandler<Operation.Data>
/// - result: The result of a performed operation. Will have a `GraphQLResult` with the data of the requested type on `success`, and an `Error` on `failure`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is needed, but we may want to mention GraphQLResult can contain GraphQL errors in addition to data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good call

@@ -26,11 +26,17 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
}

func fetch(cachePolicy: CachePolicy) {
fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context) { [weak self] (result, error) in
fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context) { [weak self] outerResult in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can find a more descriptive name than outerResult, but I don't have any great suggestions. Or maybe just result and graphQLResult?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that works - i'll try to change that throughout when there's not a 2nd result being wrapped

let websocketError = WebSocketError(payload: payload,
error: error,
kind: .neitherErrorNorPayloadReceived)
responseHandler(.failure(websocketError))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the full context here (this was contributed by someone else), but I'm wondering when this condition would actually occur. I suspect this is something that should be solved in OperationMessage.parse, because it seems like a protocol error.

} else {
XCTFail("Unexpected error: \(error)")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That definitely seems correct to me, I think the previous behavior was a mistake.


client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in
client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called innerResult here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is a nested callback - we already have outerResult above.

@mormahr
Copy link

mormahr commented Jul 19, 2019

Hey, not an expert wrt. swift, but maybe one could add the old signature with a @deprecated or something like that?

@designatednerd
Copy link
Contributor Author

@mormahr Definitely doable, but it's pretty significant maintenance overhead to maintain all of the old public method signatures and basically need to do this with every single one:

@available(*, deprecated, message: "Use the method with the `Result` completion handler instead")
func oldMethod(completion: @escaping (_ parameter: Parameter?, _ error: Error?)) {
  self.newMethod { result in 
    switch result {
    case .success(let parameter):
      completion(parameter, nil)
    case .failure(let error):
      completion(nil, error)
    }
  }
}

Unless there's a real good reason not to, I'd kinda prefer to rip the band-aid off here.

@designatednerd
Copy link
Contributor Author

OK folks - leaving this open for further comment until 10pm Netherlands Time (1pm US-Pacific) on Monday. Speak now or forever be mad at me for breaking your shit!

@designatednerd
Copy link
Contributor Author

May [deity of choice] have mercy on us all. Merging!

@designatednerd designatednerd merged commit 96305f8 into master Jul 22, 2019
@designatednerd designatednerd deleted the change/result-type-returns branch July 23, 2019 09:28
@JakeTorres
Copy link

what's wrong with that, get a schema.json, but swift API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Issues outlining new things we want to do or things that will make our lives as devs easier swift-5 Issues specific to Swift 5.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants