Connect-Swift

repository·main·Indexed 41 days ago

https://github.com/connectrpc/connect-swift

A lightweight Swift library providing type-safe, idiomatic APIs for communicating with servers via Connect, gRPC, and gRPC-Web using Protocol Buffers. It includes the Connect module for Connect and gRPC-Web, ConnectNIO for SwiftNIO-backed gRPC support, and ConnectMocks for simplifying the testing of generated clients.

Tokens
2K
Snippets
5
Records
12
Agent score
50%

What's inside Connect-Swift

  1. Overview of Connect-Swift

    main

    Connect-Swift is a lightweight library (<200KB) that enables type-safe, idiomatic Swift communication with servers using Protocol Buffers (Protobuf). It supports the Connect, gRPC, and gRPC-Web protocols.

    Instead of manually writing Codable models or handling serialization, Connect-Swift uses code generation to provide:

    • Protocol Interfaces: Swift protocols for service clients.
    • Client Implementations: Concrete classes that handle the network transport and RPC calls.
    • Mocks: Generated mock classes for easy unit testing.
  2. Use ConnectNIO for SwiftNIO-backed HTTP clients

    main

    ConnectNIO provides an NIOHTTPClient implementation that conforms to the Connect library's HTTPClientInterface protocol. It is built on top of the SwiftNIO networking stack.

    Because it leverages SwiftNIO's support for trailers, this module allows you to use the gRPC protocol in addition to the Connect and gRPC-Web protocols supported by the core Connect library.

  3. Generate Connect-Swift code using Buf

    main

    To generate RPC client stubs, use the protoc-gen-connect-swift plugin in conjunction with Apple's swift-protobuf. If you are using buf, add the buf.build/connectrpc/swift plugin to your buf.gen.yaml file.

    Recommended options:

    • GenerateAsyncMethods=true: Generates async versions of RPC methods.
    • GenerateCallbackMethods=true: Generates callback-based versions of RPC methods.
    • Visibility=Public: Ensures generated code is accessible outside the module.
    version: v2
    plugins:
      - remote: buf.build/apple/swift
        out: Generated
        opt: Visibility=Public
      - remote: buf.build/connectrpc/swift
        out: Generated
        opt:
          - GenerateAsyncMethods=true
          - GenerateCallbackMethods=true
          - Visibility=Public
  4. Explore Connect-Swift example applications

    main

    The ConnectExamples directory provides a comprehensive example application that demonstrates how to use Connect-Swift to chat with the ELIZA demo service. The example covers both unary and streaming APIs across all supported protocols.

    To see a real-world implementation using Swift Package Manager, refer to the ElizaSwiftPackageApp directory.

    // Example usage pattern demonstrated in the app:
    // - Unary APIs
    // - Streaming APIs
    // - Multiple supported protocols
  5. Install Connect-Swift via Swift Package Manager

    main

    Connect-Swift is distributed exclusively through Swift Package Manager (SPM).

    Note: CocoaPods support has been removed. While older versions (up to 1.2.3) may still be installable from CocoaPods Trunk, they will not be updated.

  6. How to use ConnectMocks for testing

    main

    ConnectMocks provides types to simplify mocking and testing generated Connect clients and methods. To use them, follow this workflow:

    1. Generate Mocks: Run the connect-swift-mocks plugin during your code generation step.
    2. Output Files: The plugin generates *.mock.swift files. These files import ConnectMocks to provide default no-op implementations for every generated service and rpc.
    3. Setup Test Suite: Import both the ConnectMocks module and your generated *.mock.swift files into your test target.
    4. Dependency Injection: Ensure your application code depends on *Interface types rather than concrete implementation types. This allows you to inject the generated *Mock types into your code during tests, replacing the production *Client types and allowing you to provide mocked test data instead of making real RPC calls.
    // Conceptual workflow example:
    
    // 1. In your production code, use the Interface:
    // func performAction(client: MyServiceInterface) { ... }
    
    // 2. In your test code, inject the Mock:
    // let mock = MyServiceMock()
    // performAction(client: mock)
    
    // 3. Configure the mock with test data (provided by ConnectMocks implementations)
    // mock.someMethod.responses.append(.success(myTestData))
  7. Generate Connect-Swift code using protoc

    main

    If you are using protoc directly instead of buf, add the --connect-swift_out flag to your invocation. You must also provide corresponding --connect-swift_opt flags to configure the generator.

    protoc --swift_out=. --connect-swift_out=. \
        --swift_opt=Visibility=Public \
        --connect-swift_opt=GenerateAsyncMethods=true,GenerateCallbackMethods=true,Visibility=Public \
        my.proto
  8. Explore ElizaSwiftPackageApp example

    main

    The ElizaSwiftPackageApp is a demonstration application that shows how to integrate the Connect library into a Swift project using Swift Package Manager (SPM). It provides a functional interface for chatting with the Eliza service.

    The example demonstrates support for multiple protocols and interaction patterns:

    • Connect Protocol: Supports both unary and streaming calls.
    • gRPC Protocol: Supports both unary and streaming calls (implemented using ConnectGRPC and SwiftNIO).
    • gRPC-Web Protocol: Supports both unary and streaming calls.
  9. Mocking and testing RPC clients

    main

    Connect-Swift generates mock classes that conform to the same service interfaces as the production clients. This allows you to inject mocks into your ViewModels or services for unit testing. You can define custom behavior for the mock methods using closures.

    // Example of testing a ViewModel with a generated mock
    func testMessagingViewModel() async {
        let client = Eliza_V1_ChatServiceClientMock()
        
        // Define the mock behavior
        client.mockAsyncSay = { request in
            XCTAssertEqual(request.sentence, "hello!")
            return ResponseMessage(result: .success(.with { $0.sentence = "hi, i'm eliza!" }))
        }
    
        let viewModel = MessagingViewModel(elizaClient: client)
        await viewModel.send("hello!")
    
        XCTAssertEqual(viewModel.messages.count, 2)
    }