Swiftpack.co - Package - vyshane/grpc-swift-combine

CombineGRPC

CombineGRPC is a library that provides Combine framework integration for Swift gRPC.

CombineGRPC provides two flavours of functionality, call and handle. Use call to make gRPC calls on the client side, and handle to handle incoming requests on the server side. The library provides versions of call and handle for all RPC styles. Here are the input and output types for each.

RPC Style | Input and Output Types --- | --- Unary | Request -> AnyPublisher<Response, RPCError> Server streaming | Request -> AnyPublisher<Response, RPCError> Client streaming | AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError> Bidirectional streaming | AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError>

When you make a unary call, you provide a request message, and get back a response publisher. The response publisher will either publish a single response, or fail with a RPCError error. Similarly, if you are handling a unary RPC call, you provide a handler that takes a request parameter and returns an AnyPublisher<Response, RPCError>.

You can follow the same intuition to understand the types for the other RPC styles. The only difference is that publishers for the streaming RPCs may publish zero or more messages instead of the single response message that is expected from the unary response publisher.

Quick Tour

Let's see a quick example. Consider the following protobuf definition for a simple echo service. The service defines one bidirectional RPC. You send it a stream of messages and it echoes the messages back to you.

syntax = "proto3";

service EchoService {
  rpc SayItBack (stream EchoRequest) returns (stream EchoResponse);
}

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
}

Server Side

To implement the server, you provide a handler function that takes an input stream AnyPublisher<EchoRequest, Error> and returns an output stream AnyPublisher<EchoResponse, RPCError>.

import Foundation
import Combine
import CombineGRPC
import GRPC
import NIO

class EchoServiceProvider: EchoProvider {
  
  // Simple bidirectional RPC that echoes back each request message
  func sayItBack(context: StreamingResponseCallContext<EchoResponse>) -> EventLoopFuture<(StreamEvent<EchoRequest>) -> Void> {
    handle(context) { requests in
      requests
        .map { req in
          EchoResponse.with { $0.message = req.message }
        }
        .setFailureType(to: RPCError.self)
        .eraseToAnyPublisher()
    }
  }
}

Start the server. This is the same process as with Swift gRPC.

let configuration = Server.Configuration(
  target: ConnectionTarget.hostAndPort("localhost", 8080),
  eventLoopGroup: PlatformSupport.makeEventLoopGroup(loopCount: 1),
  serviceProviders: [EchoServiceProvider()]
)
_ = try Server.start(configuration: configuration).wait()

Client Side

Now let's setup our client. Again, it's the same process that you would go through when using Swift gRPC.

let eventLoopGroup = PlatformSupport.makeEventLoopGroup(loopCount: eventLoopGroupSize)
let channel = ClientConnection
  .insecure(group: eventLoopGroup)
  .connect(host: "localhost", port: 8080)
let echoClient = EchoServiceClient(channel: channel)

To call the service, create a GRPCExecutor and use its call method. You provide it with a stream of requests AnyPublisher<EchoRequest, Error> and you get back a stream AnyPublisher<EchoResponse, RPCError> of responses from the server.

let requests = repeatElement(EchoRequest.with { $0.message = "hello"}, count: 10)
let requestStream: AnyPublisher<EchoRequest, Error> =
  Publishers.Sequence(sequence: requests).eraseToAnyPublisher()
let grpc = GRPCExecutor()

grpc.call(echoClient.sayItBack)(requestStream)
  .filter { $0.message == "hello" }
  .count()
  .sink(receiveValue: { count in
    assert(count == 10)
  })

That's it! You have set up bidirectional streaming between a server and client. The method sayItBack of EchoServiceClient is generated by Swift gRPC. Notice that call is curried. You can preselect RPC calls using partial application:

let sayItBack = grpc.call(echoClient.sayItBack)

sayItBack(requestStream).map { response in
  // ...
}

Configuring RPC Calls

The GRPCExecutor allows you to configure CallOptions for your RPC calls. You can provide the GRPCExecutor's initializer with a stream AnyPublisher<CallOptions, Never>, and the latest CallOptions value will be used when making calls.

let timeoutOptions = CallOptions(timeout: try! .seconds(5))
let grpc = GRPCExecutor(callOptions: Just(timeoutOptions).eraseToAnyPublisher())

Retry Policy

You can also configure GRPCExecutor to automatically retry failed calls by specifying a RetryPolicy. In the following example, we retry calls that fail with status .unauthenticated. We use CallOptions to add a Bearer token to the authorization header, and then retry the call.

// Default CallOptions with no authentication
let callOptions = CurrentValueSubject<CallOptions, Never>(CallOptions())

let grpc = GRPCExecutor(
  callOptions: callOptions.eraseToAnyPublisher(),
  retry: .failedCall(
    upTo: 1,
    when: { error in
      error.status.code == .unauthenticated
    },
    delayUntilNext: { retryCount in  // Useful for implementing exponential backoff
      // Retry the call with authentication
      callOptions.send(CallOptions(customMetadata: HTTPHeaders([("authorization", "Bearer xxx")])))
      return Just(()).eraseToAnyPublisher()
    },
    didGiveUp: {
      print("Authenticated call failed.")
    }
  )
)

grpc.call(client.authenticatedRpc)(request)
  .map { response in
    // ...
  }

You can imagine doing something along those lines to seamlessly retry calls when an ID token expires. The back-end service replies with status .unauthenticated, you obtain a new ID token using your refresh token, and the call is retried.

More Examples

Check out the CombineGRPC tests for examples of all the different RPC calls and handlers implementations. You can find the matching protobuf here.

Logistics

Generating Swift Code from Protobuf

To generate Swift code from your .proto files, you'll need to first install the protoc Protocol Buffer compiler.

brew install protobuf

Next, download the swift and grpc-swift protoc plugins from the the latest version of grpc-swift. Currently that means protoc-grpc-swift-plugins-1.0.0-alpha.19.zip . Unarchive the downloaded file and move the binaries from the bin/ directory somewhere in your $PATH.

Now you are ready to generate Swift code from protobuf interface definition files.

Let's generate the message types, gRPC server and gRPC client for Swift.

protoc example_service.proto --swift_out=Generated/
protoc example_service.proto --grpc-swift_out=Generated/

You'll see that protoc has created two source files for us.

ls Generated/
example_service.grpc.swift
example_service.pb.swift

Adding CombineGRPC to Your Project

You can easily add CombineGRPC to your project using either Swift Package Manager or CocoaPods.

Swift Package Manager

Add the package dependency to your Package.swift:

dependencies: [
  .package(url: "https://github.com/vyshane/grpc-swift-combine.git", from: "0.18.0"),
],

CocoaPods

Add the following line to your Podfile:

pod 'CombineGRPC', '~> 0.18'

Compatibility

Since this library integrates with Combine, it only works on platforms that support Combine. This currently means the following minimum versions:

Platform | Minimum Supported Version --- | --- macOS | 10.15 (Catalina) iOS & iPadOS | 13 tvOS | 13

WatchOS is not supported because upstream gRPC Swift does not support it.

Feature Status

RPC Client Calls

  • ☑ Unary
  • ☑ Client streaming
  • ☑ Server streaming
  • ☑ Bidirectional streaming
  • ☑ Retry policy for automatic client call retries

Server Side Handlers

  • ☑ Unary
  • ☑ Client streaming
  • ☑ Server streaming
  • ☑ Bidirectional streaming

End-to-end Tests

  • ☑ Unary
  • ☑ Client streaming
  • ☑ Server streaming
  • ☑ Bidirectional streaming

Contributing

To generate the Xcode project, run:

make project

You can then open the project in Xcode, build and run the tests.

Github

link
Stars: 32

Used By

Total: 0

Releases

0.18.0 - 2020-09-18 14:11:17

This release contains backwards-incompatible API changes. Instead of GRPCStatus, RPCs now signal failure through a new RPCError type. The RPCError type contains the GRPCStatus as well as trailing metadata.

The RPC call and handle functions are now based on the following signatures:

RPC Style | Input and Output Types --- | --- Unary | Request -> AnyPublisher<Response, RPCError> Server streaming | Request -> AnyPublisher<Response, RPCError> Client streaming | AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError> Bidirectional streaming | AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError>

RetryPolicy.when also now provides RPCError instead of GRPCStatus to its predicate. I.e. when now has the signature RPCError -> Bool

0.17.0 - 2020-09-02 12:38:59

Update Swift gRPC to v1.0.0-alpha.19. CombineGRPC is now also available as a CocoaPod.

0.16.0 - 2020-08-18 14:17:11

Update Swift gRPC to v1.0.0-alpha.18.

0.15.0 - 2020-08-04 14:05:06

Update Swift gRPC to v1.0.0-alpha.17.

0.14.0 - 2020-07-31 14:05:16

Update Swift gRPC to v1.0.0-alpha.16.

0.13.0 - 2020-06-11 14:00:54

Update Swift gRPC to v1.0.0-alpha.14.

0.12.0 - 2020-06-10 13:04:26

Update Swift gRPC to v1.0.0-alpha.13.

0.11.0 - 2020-05-13 13:40:38

Update Swift gRPC to v1.0.0-alpha.12.

0.10.0 - 2020-04-18 08:01:59

Update Swift gRPC to v1.0.0-alpha.11. This version of Swift gRPC has breaking API changes. You will need to regenerate Swift sources from your protobuf.

Since upstream has breaking changes, I have also taken the opportunity to rename the RetryPolicy.failedCall onGiveUp parameter to didGiveUp, which is more idiomatic Swift.

0.9.0 - 2020-01-29 00:21:16

Update Swift gRPC to v1.0.0-alpha.9.

0.8.0 - 2019-11-16 10:25:17

Update Swift gRPC to v1.0.0-alpha.7.

0.7.1 - 2019-11-02 03:41:59

Handle client stream error with correct RPC cancellation.

0.7.0 - 2019-09-18 23:34:26

Update Swift gRPC to v1.0.0-alpha.6. Update platforms support.

0.6.0 - 2019-09-11 14:11:12

Quick help (inline) documentation. Access level fixes.

0.5.0 - 2019-09-09 14:32:38

RetryPolicy's delayUntilNext now provides the current retry count. This is useful for implementing exponential backoff.

0.4.0 - 2019-09-08 15:53:05

Automatic client call retries, configured using retry policy. Updated upstream Swift gRPC dependency to v1.0.0-alpha.5.

- 2019-08-20 14:43:30

Updated upstream Swift gRPC dependency to v1.0.0-alpha.4.