# 1 - Promisekit
```Swift
// ContentAPI.swift
func getLandingPage(uid: String) -> Promise<GetLandingPageByUidResponse> {
prepare(service: service)
var request = GetLandingPageByUidRequest()
request.uid = uid
let promise: Promise<GetLandingPageByUidResponse> = Promise { seal in
_ = try? service!.getLandingPageByUid(request) { response, result in
if result.success {
seal.resolve(response, nil)
} else {
seal.reject(self.buildError(callResult: result)!)
}
}
}
return attempt { promise }
// LandingPageModelController.swift
func loadLandingPage(_ completion: @escaping () -> Void) {
let uid = landingPageUid
contentApi.getLandingPage(uid: landingPageUid)
.done { (response) in
self.landingPage = response.page
self.landingPageIncludes = response.includes
completion()
}.catch { _ in completion() }
```
Note: In the above example I did not refactor the completion handler, this is because I didn't feel like it. In reality we'd likely want to return the promise up to the view and let it do it's async behavior also (I think it's possible)
## Pros
- Clean API
- Easy retrying
- Better API for cascading or concurrent calls
- Large community around `PromiseKit`
- We can use promises in non-network places like alert callbacks
## Cons
- Dependency
- Has a non-matching API to iOS 13 Future protocol making a future upgrade to Combine difficult
- Unfamiliarity
- Promises would need to be hand written or generated
# 2 - Open Combine
Open Combine is a cross-platform framework that intends to match the combine API for any platform. We only really need `Future` because that's all we need.
```swift
Future<GetLandingPageByUidResponse, NetworkError> { promise in
_ = try? self.service.getLandingPageByUid(request, completion: { (response, result) in
promise(self.prepareResult(response: response, result: result))
})
}
```
## Pros
- It matches iOS 13 API making future transition hopefully seamless
- Meets our needs
## Cons
- Very little ergonomic win over something more tested like `PromiseKit`
- Bringing in a dependency like this feels very risky
# 3 - Hand Rolled Object
```swift
class RetryableRequest<Request, Response, Call> {
typealias UnvalidatedResponse = (Response?, CallResult) -> Void
typealias ValidatedResponse = (Result<Response, NetworkError>) -> Void
var response: Response?
var result: CallResult?
var retryCount = 0
let resultHandler: ValidatedResponse
let request: (@escaping UnvalidatedResponse) -> ClientCallUnary?
init(grpcRequest: @escaping(Request, @escaping UnvalidatedResponse) throws -> ClientCallUnary, req: Request, resultHandler: @escaping ValidatedResponse) {
self.resultHandler = resultHandler
self.request = { try? grpcRequest(req, $0) }
run()
}
func run() {
_ = request(completionHandler)
}
func completionHandler(response: Response?, result: CallResult) {
shouldRetry(result) ? self.retry(result) : resultHandler(validate(response: response, result: result))
}
func retry(_ result: CallResult) {
run()
retryCount += 1
}
func shouldRetry(_ result: CallResult) -> Bool {
return retryCount <= 3 && !result.success
}
func validate(response: Response?, result: CallResult) -> Result<Response, NetworkError> {
if let error = buildError(callResult: result) { return .failure(error) }
if let response = response { return .success(response) }
var error: NetworkError {
return NetworkError(result: CallResult(success: false,
statusCode: StatusCode.unknown,
statusMessage: nil,
resultData: nil,
initialMetadata: nil,
trailingMetadata: nil),
statusMessage: nil, host: "", whiteSpaceUsernameErr: false)
}
return .failure(error)
}
func parse(callResult: CallResult) -> NetworkError? {
guard callResult.statusCode != .unimplemented else {
// showUpgradeAlert()
return nil
}
guard callResult.statusCode != .ok else { return nil }
return NetworkError(result: callResult,
statusMessage: callResult.statusMessage,
host: "",
whiteSpaceUsernameErr: false)
}
func buildError(callResult: CallResult) -> NetworkError? {
guard let result = parse(callResult: callResult) else {
/// This forces an error.
// return NetworkError(result: callResult, statusMessage: "Dead meat", host: self.host, whiteSpaceUsernameErr: self.whiteSpaceUsernameErr)
return nil
}
return result
}
}
// Callsite
let requ: RetryableRequest = RetryableRequest<GetLandingPageByUidRequest, GetLandingPageByUidResponse, Result<GetLandingPageByUidResponse, NetworkError>>(
grpcRequest: self.service!.getLandingPageByUid(_:completion:),
req: request,
resultHandler: completion)
```
## Pros
- We own the object
- Meets our needs
- We can do some things like a `Promise` from `Promisekit` like adding `done` method but we can allow multiple giving us the ability to let multiple files along the way listen for completion events (this does get us close in functionaltiy to Rx)
## Cons
- Cumbersome API
- lots of unfamiliar looking code
- Autcomplete hates it
# 4 - Recursion
This doesn't seem to be a real solution. The previous implementation in legacy appears to have been a different type of gRPC or version. There looks to be some kind of request object that was used, this doesn't appear anywhere in our current codebase. (There _could_ be a solution involving not using our current gRPC files but instead dropping down lower in the gRPC stack. I don't think it's wise)
# 5 - Code Gen
In theory we can use codegen to take our current gRPC files and generate. I see no reason why this would not be possible but it will require further research that will likely go beyond the time box of this spike.
After some investigation codegen could be a solid solution with a few gotchas
## Pros
- Little to no work involved in creating a new API
- We can quickly create new implementations during future development
- Code gen can be used in conjunction with any of the previous methods to generate the API quicky
## Cons
- Depending on what we use it can lead to headaches Sourcery for example uses a templating language call `StencilKit` which has a learning curve and will take some core-team learning to get the whole thing or we could use Apple's officially blessed `SwiftSyntax` (https://github.com/apple/swift-syntax) which is all Swift but I'm unsure of how powerful it is as I haven't used it.
- Codegen can be brittle if templates aren't robust enough, a single change that's unexpected by the code generator can create errors which leads to core-team then needing to update the templates
- This only works for currently generated proto definitions, any custom API's we'd need to build by hand
- If any future proto definitions deviate from the current function signature (we got lucky that almost every signature is `func call...(request: Request, completion: (Response?, CallResult) -> Void)`) we'll need to reasses our code generation
- Generated code can be difficult to read
- Generated code can't be edited
```stencil
import SwiftProtobuf
import ChewyProtobuf
// swiftlint:disable all
{% for type in types.inheriting.ServiceClientBase %}
class {{ type.name }}API: APIGroup {
{# {% for var in type.variables %} #}
{% for function in type.methods %}
{% if function.returnTypeName | contains: "Call" and function.parameters%}
func {{ function.callName }} (
{% for param in function.parameters %}
{% if param.name|contains: "request" %}
{{ param.name }}:{{ param.typeName }},
{% elif param.name|contains: "completion" %}
{# {{ param }} #}
completion: @escaping (Result<{{ param.typeName.closure.parameters.first.type.name }}, NetworkError>) -> Void
{% endif %}
{% endfor %}
) {
_ = try? self.service?.{{ function.callName }}(request, validateCompletion(completion))
}
{% endif %}
{% endfor %}
}
{% endfor %}
```
Generates
```swift
class AddressValidationServiceClientAPI: APIGroup {
func verifyAddress (
request:AVRequest,
completion: @escaping (Result<AVResponse, NetworkError>) -> Void
) {
_ = try? self.service?.verifyAddress(request, validateCompletion(completion))
}
func verifyBulkAddress (
request:AVBulkRequest,
completion: @escaping (Result<AVBulkResponse, NetworkError>) -> Void
) {
_ = try? self.service?.verifyBulkAddress(request, validateCompletion(completion))
}
func suggestAddresses (
request:AVAddressSuggestion,
completion: @escaping (Result<AVBulkResponse, NetworkError>) -> Void
) {
_ = try? self.service?.suggestAddresses(request, validateCompletion(completion))
}
}
```
# Other Things to Consider
- grpcSwift is deprecating the current iteration in favor of Swift-Nio