├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── dependabot.yml ├── pull_request_template.md ├── release.yml └── workflows │ ├── add-to-project.yml │ ├── ci.yml │ ├── pr-title.yml │ └── release.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDETemplateMacros.plist ├── Connect-Swift-Mocks.podspec ├── Connect-Swift.podspec ├── Examples ├── ElizaCocoaPodsApp │ ├── ElizaCocoaPodsApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── ElizaCocoaPodsApp.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── ElizaCocoaPodsApp │ │ └── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ │ └── Contents.json │ ├── Podfile │ ├── Podfile.lock │ └── README.md ├── ElizaSharedSources │ ├── AppSources │ │ ├── ElizaApp.swift │ │ ├── MenuView.swift │ │ ├── Message.swift │ │ ├── MessagingView.swift │ │ └── MessagingViewModel.swift │ ├── GeneratedSources │ │ └── connectrpc │ │ │ └── eliza │ │ │ └── v1 │ │ │ ├── eliza.connect.swift │ │ │ └── eliza.pb.swift │ └── README.md ├── ElizaSwiftPackageApp │ ├── ElizaSwiftPackageApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── ElizaSwiftPackageApp │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Info.plist │ └── README.md ├── README.md └── buf.gen.yaml ├── LICENSE ├── Libraries ├── Connect │ ├── Internal │ │ ├── GeneratedSources │ │ │ └── proto │ │ │ │ └── grpc │ │ │ │ └── status │ │ │ │ └── v1 │ │ │ │ └── status.pb.swift │ │ ├── Interceptors │ │ │ ├── ConnectInterceptor.swift │ │ │ ├── GRPCWebInterceptor.swift │ │ │ └── InterceptorChain.swift │ │ ├── Streaming │ │ │ ├── BidirectionalAsyncStream.swift │ │ │ ├── BidirectionalStream.swift │ │ │ ├── ClientOnlyAsyncStream.swift │ │ │ ├── ClientOnlyStream.swift │ │ │ ├── ClientOnlyStreamValidation.swift │ │ │ ├── ConnectEndStreamResponse.swift │ │ │ ├── ServerOnlyAsyncStream.swift │ │ │ ├── ServerOnlyStream.swift │ │ │ └── URLSessionStream.swift │ │ ├── Unary │ │ │ └── UnaryAsyncWrapper.swift │ │ └── Utilities │ │ │ ├── Lock.swift │ │ │ ├── Locked.swift │ │ │ └── TimeoutTimer.swift │ ├── PackageInternal │ │ ├── ConnectError+GRPC.swift │ │ ├── Envelope.swift │ │ ├── Headers+GRPC.swift │ │ └── Trailers+gRPC.swift │ ├── Public │ │ ├── Implementation │ │ │ ├── Clients │ │ │ │ ├── ProtocolClient.swift │ │ │ │ └── URLSessionHTTPClient.swift │ │ │ ├── Codecs │ │ │ │ ├── JSONCodec.swift │ │ │ │ └── ProtoCodec.swift │ │ │ └── Compression │ │ │ │ └── GzipCompressionPool.swift │ │ └── Interfaces │ │ │ ├── Cancelable.swift │ │ │ ├── Code.swift │ │ │ ├── Codec.swift │ │ │ ├── CompressionPool.swift │ │ │ ├── ConnectError.swift │ │ │ ├── HTTPClientInterface.swift │ │ │ ├── HTTPMethod.swift │ │ │ ├── HTTPMetrics.swift │ │ │ ├── HTTPRequest.swift │ │ │ ├── HTTPResponse.swift │ │ │ ├── HeaderConstants.swift │ │ │ ├── Headers.swift │ │ │ ├── IdempotencyLevel.swift │ │ │ ├── Interceptors │ │ │ ├── Interceptor.swift │ │ │ ├── InterceptorFactory.swift │ │ │ ├── StreamInterceptor.swift │ │ │ └── UnaryInterceptor.swift │ │ │ ├── MethodSpec.swift │ │ │ ├── NetworkProtocol.swift │ │ │ ├── ProtobufMessage.swift │ │ │ ├── ProtocolClientConfig.swift │ │ │ ├── ProtocolClientInterface.swift │ │ │ ├── ResponseMessage.swift │ │ │ ├── Streaming │ │ │ ├── AsyncAwait │ │ │ │ ├── BidirectionalAsyncStreamInterface.swift │ │ │ │ ├── ClientOnlyAsyncStreamInterface.swift │ │ │ │ └── ServerOnlyAsyncStreamInterface.swift │ │ │ ├── Callbacks │ │ │ │ ├── BidirectionalStreamInterface.swift │ │ │ │ ├── ClientOnlyStreamInterface.swift │ │ │ │ ├── RequestCallbacks.swift │ │ │ │ ├── ResponseCallbacks.swift │ │ │ │ └── ServerOnlyStreamInterface.swift │ │ │ └── StreamResult.swift │ │ │ └── Trailers.swift │ ├── README.md │ ├── buf.gen.yaml │ └── proto │ │ ├── README.md │ │ ├── buf.yaml │ │ └── grpc │ │ └── status │ │ └── v1 │ │ └── status.proto ├── ConnectMocks │ ├── MockBidirectionalAsyncStream.swift │ ├── MockBidirectionalStream.swift │ ├── MockClientOnlyAsyncStream.swift │ ├── MockClientOnlyStream.swift │ ├── MockServerOnlyAsyncStream.swift │ ├── MockServerOnlyStream.swift │ └── README.md └── ConnectNIO │ ├── Internal │ ├── ConnectStreamChannelHandler.swift │ ├── ConnectUnaryChannelHandler.swift │ ├── Extensions │ │ ├── ConnectError+Extensions.swift │ │ ├── HTTPRequestHead+Extensions.swift │ │ └── Headers+Extensions.swift │ └── GRPCInterceptor.swift │ ├── Public │ ├── NIOHTTPClient.swift │ └── NetworkProtocol+Extensions.swift │ └── README.md ├── MAINTAINERS.md ├── Makefile ├── Package.resolved ├── Package.swift ├── Plugins ├── ConnectMocksPlugin │ └── ConnectMockGenerator.swift ├── ConnectPluginUtilities │ ├── FilePathComponents.swift │ ├── Generator.swift │ ├── GeneratorOptions.swift │ ├── MethodDescriptor+Extensions.swift │ └── ServiceDescriptor+Extensions.swift └── ConnectSwiftPlugin │ └── ConnectClientGenerator.swift ├── README.md ├── SECURITY.md └── Tests ├── ConformanceClient ├── GeneratedSources │ └── connectrpc │ │ └── conformance │ │ └── v1 │ │ ├── client_compat.pb.swift │ │ ├── config.pb.swift │ │ ├── server_compat.pb.swift │ │ ├── service.connect.swift │ │ ├── service.pb.swift │ │ └── suite.pb.swift ├── InvocationConfigs │ ├── nio.yaml │ └── urlsession.yaml ├── README.md ├── Sources │ ├── CommandLineArgument.swift │ ├── ConformanceInvoker.swift │ └── main.swift └── buf.gen.yaml └── UnitTests ├── ConnectLibraryTests ├── ConnectMocksTests │ └── ConnectMocksTests.swift ├── ConnectTests │ ├── ConnectEndStreamResponseTests.swift │ ├── ConnectErrorTests.swift │ ├── EnvelopeTests.swift │ ├── GzipCompressionPoolTests.swift │ ├── InterceptorChainIterationTests.swift │ ├── InterceptorFactoryTests.swift │ ├── InterceptorIntegrationTests.swift │ ├── JSONCodecTests.swift │ ├── ProtoCodecTests.swift │ ├── ProtocolClientConfigTests.swift │ └── ServiceMetadataTests.swift ├── GeneratedSources │ └── connectrpc │ │ └── conformance │ │ └── v1 │ │ ├── client_compat.pb.swift │ │ ├── config.pb.swift │ │ ├── server_compat.pb.swift │ │ ├── service.connect.swift │ │ ├── service.mock.swift │ │ ├── service.pb.swift │ │ └── suite.pb.swift ├── TestResources │ └── gzip-test.txt.gz └── buf.gen.yaml ├── ConnectPluginUtilitiesTests └── FilePathComponentsTests.swift └── README.md /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Community Code of Conduct 2 | 3 | Connect follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Before submitting your PR:** Please read through the contribution guide at https://github.com/connectrpc/connect-swift/blob/main/.github/CONTRIBUTING.md 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - dependabot 7 | categories: 8 | - title: Enhancements 9 | labels: 10 | - enhancement 11 | - title: Bugfixes 12 | labels: 13 | - bug 14 | - title: Other changes 15 | labels: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues and PRs to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | - transferred 9 | pull_request_target: 10 | types: 11 | - opened 12 | - reopened 13 | issue_comment: 14 | types: 15 | - created 16 | 17 | jobs: 18 | call-workflow-add-to-project: 19 | name: Call workflow to add issue to project 20 | uses: connectrpc/base-workflows/.github/workflows/add-to-project.yaml@main 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: {} # support manual runs 8 | env: 9 | # Sets the Xcode version to use for the CI. 10 | # Available Versions: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode 11 | # Ref: https://www.jessesquires.com/blog/2020/01/06/selecting-an-xcode-version-on-github-ci/ 12 | DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer 13 | permissions: 14 | contents: read 15 | jobs: 16 | build-eliza-cocoapods-example: 17 | runs-on: macos-15 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Build Eliza CocoaPods example 21 | run: | 22 | cd Examples/ElizaCocoaPodsApp 23 | pod install 24 | set -o pipefail && xcodebuild -workspace ElizaCocoaPodsApp.xcworkspace -scheme ElizaCocoaPodsApp build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcbeautify 25 | build-eliza-swiftpm-example: 26 | runs-on: macos-15 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Build Eliza Swift PM example 30 | run: | 31 | cd Examples/ElizaSwiftPackageApp 32 | set -o pipefail && xcodebuild -scheme ElizaSwiftPackageApp build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcbeautify 33 | build-library-ios: 34 | runs-on: macos-15 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Build Connect iOS library 38 | run: set -o pipefail && xcodebuild -scheme Connect-Package -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' | xcbeautify 39 | build-library-macos: 40 | runs-on: macos-15 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Build Connect macOS library 44 | run: set -o pipefail && xcodebuild -scheme Connect-Package -destination 'platform=macOS' | xcbeautify 45 | build-library-tvos: 46 | runs-on: macos-15 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Build Connect tvOS library 50 | run: set -o pipefail && xcodebuild -scheme Connect-Package -destination 'platform=tvOS Simulator,name=Apple TV,OS=18.4' | xcbeautify 51 | build-library-watchos: 52 | runs-on: macos-15 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Build Connect watchOS library 56 | run: set -o pipefail && xcodebuild -scheme Connect-Package -destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=11.0' | xcbeautify 57 | build-plugin-and-generate: 58 | runs-on: macos-15 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: bufbuild/buf-setup-action@v1.50.0 62 | with: 63 | github_token: ${{ github.token }} 64 | - name: Build plugins 65 | run: make buildplugins 66 | - name: Generate outputs 67 | run: make generate 68 | - name: Ensure no generated diff 69 | run: | 70 | git update-index --refresh --add --remove 71 | git diff-index --quiet HEAD -- 72 | run-conformance-tests: 73 | runs-on: macos-15 74 | steps: 75 | - uses: actions/checkout@v4 76 | - name: Install conformance runner 77 | run: make installconformancerunner 78 | - name: Run conformance tests 79 | run: make testconformance 80 | run-unit-tests: 81 | runs-on: macos-15 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: actions/setup-go@v5 85 | with: 86 | go-version: 1.21.x 87 | - name: Run unit tests 88 | run: make testunit 89 | run-swiftlint: 90 | runs-on: ubuntu-latest 91 | container: 92 | image: ghcr.io/realm/swiftlint:0.58.2 93 | steps: 94 | - uses: actions/checkout@v4 95 | - name: Run SwiftLint 96 | run: swiftlint lint --strict 97 | validate-license-headers: 98 | runs-on: ubuntu-latest 99 | steps: 100 | - uses: actions/checkout@v4 101 | - name: Validate license headers 102 | run: | 103 | make licenseheaders 104 | git update-index --refresh 105 | git diff-index --quiet HEAD -- 106 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR Title 2 | # Prevent writing to the repository using the CI token. 3 | # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions 4 | permissions: 5 | pull-requests: read 6 | on: 7 | pull_request: 8 | # By default, a workflow only runs when a pull_request's activity type is opened, 9 | # synchronize, or reopened. We explicity override here so that PR titles are 10 | # re-linted when the PR text content is edited. 11 | types: 12 | - opened 13 | - edited 14 | - reopened 15 | - synchronize 16 | jobs: 17 | lint: 18 | uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: {} # support manual runs 7 | env: 8 | # Sets the Xcode version to use for the CI. 9 | # Available Versions: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode 10 | # Ref: https://www.jessesquires.com/blog/2020/01/06/selecting-an-xcode-version-on-github-ci/ 11 | DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer 12 | permissions: 13 | contents: write 14 | jobs: 15 | release: 16 | runs-on: macos-15 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: bufbuild/buf-setup-action@v1.50.0 20 | with: 21 | github_token: ${{ github.token }} 22 | - name: Build plugins 23 | run: make buildplugins 24 | - name: Zip artifacts 25 | run: | 26 | cd ./.tmp/bin 27 | mkdir ./artifacts 28 | tar -zcvf ./artifacts/protoc-gen-connect-swift.tar.gz ./protoc-gen-connect-swift 29 | tar -zcvf ./artifacts/protoc-gen-connect-swift-mocks.tar.gz ./protoc-gen-connect-swift-mocks 30 | cd ./artifacts 31 | for file in $(find . -maxdepth 1 -type f | sed 's/^\.\///' | sort | uniq); do 32 | shasum -a 256 "${file}" >> sha256.txt 33 | done 34 | - name: Publish release 35 | uses: softprops/action-gh-release@v2 36 | with: 37 | generate_release_notes: true 38 | append_body: true 39 | files: | 40 | ./.tmp/bin/artifacts/* 41 | publish-podspecs: 42 | runs-on: macos-15 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Publish podspecs to CocoaPods 46 | env: 47 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 48 | # Note that --synchronous is used since Mocks depends on the primary spec. 49 | run: | 50 | pod trunk push Connect-Swift.podspec --allow-warnings --synchronous 51 | pod trunk push Connect-Swift-Mocks.podspec --allow-warnings --synchronous 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcscheme 2 | *.xcsettings 3 | *.xcuserdatad 4 | *.xcuserstate 5 | *.xcworkspacedata 6 | .build 7 | .DS_Store 8 | /.tmp 9 | DerivedData 10 | Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp.xcworkspace 11 | Pods 12 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Examples/ElizaSharedSources/AppSources 3 | - Libraries 4 | - Plugins 5 | - Tests 6 | excluded: 7 | - Libraries/Connect/Internal/GeneratedSources 8 | - Tests/ConformanceClient/GeneratedSources 9 | - Tests/UnitTests/ConnectLibraryTests/GeneratedSources 10 | disabled_rules: 11 | - blanket_disable_command 12 | - cyclomatic_complexity 13 | - file_length 14 | - function_body_length 15 | - function_parameter_count 16 | - identifier_name 17 | - opening_brace 18 | - nesting 19 | - redundant_string_enum_value 20 | - todo 21 | - type_body_length 22 | - type_name 23 | - unavailable_function 24 | opt_in_rules: 25 | - array_init 26 | - attributes 27 | - closure_end_indentation 28 | - closure_spacing 29 | - collection_alignment 30 | - contains_over_filter_count 31 | - contains_over_filter_is_empty 32 | - contains_over_first_not_nil 33 | - contains_over_range_nil_comparison 34 | - discouraged_object_literal 35 | - empty_collection_literal 36 | - empty_count 37 | - empty_string 38 | - empty_xctest_method 39 | - enum_case_associated_values_count 40 | - explicit_init 41 | - fallthrough 42 | - fatal_error_message 43 | - file_name 44 | - first_where 45 | - flatmap_over_map_reduce 46 | - identical_operands 47 | - inclusive_language 48 | - joined_default_parameter 49 | - legacy_random 50 | - let_var_whitespace 51 | - last_where 52 | - literal_expression_end_indentation 53 | - lower_acl_than_parent 54 | - modifier_order 55 | - nimble_operator 56 | - nslocalizedstring_key 57 | - number_separator 58 | - operator_usage_whitespace 59 | - overridden_super_call 60 | - override_in_extension 61 | - prefer_self_in_static_references 62 | - private_action 63 | - prohibited_super_call 64 | - quick_discouraged_call 65 | - quick_discouraged_focused_test 66 | - quick_discouraged_pending_test 67 | - reduce_into 68 | - redundant_nil_coalescing 69 | - redundant_type_annotation 70 | - single_test_class 71 | - sorted_first_last 72 | - sorted_imports 73 | - static_operator 74 | - toggle_bool 75 | - unavailable_function 76 | - unneeded_parentheses_in_closure_argument 77 | - vertical_parameter_alignment_on_call 78 | - vertical_whitespace_closing_braces 79 | - vertical_whitespace_opening_braces 80 | - xct_specific_matcher 81 | - yoda_condition 82 | trailing_whitespace: 83 | ignores_comments: false 84 | ignores_empty_lines: false 85 | trailing_comma: 86 | mandatory_comma: true 87 | line_length: 100 88 | private_over_fileprivate: 89 | validate_extensions: true 90 | modifier_order: 91 | preferred_modifier_order: 92 | - acl 93 | - setterACL 94 | - final 95 | - override 96 | - required 97 | - typeMethods 98 | - mutators 99 | - owned 100 | - lazy 101 | - dynamic 102 | - convenience 103 | deployment_target: 104 | iOS_deployment_target: 12.0 105 | 106 | custom_rules: 107 | newline_after_brace: 108 | name: "Opening braces shouldn't have empty lines under them" 109 | regex: '\{\n\n' 110 | newline_before_brace: 111 | name: "Closing braces shouldn't have empty lines before them" 112 | regex: '\n\n\}' 113 | sendable_order: 114 | name: "@escaping should precede @Sendable when used together" 115 | regex: '@Sendable\s+@escaping' 116 | space_before_comma: 117 | name: "Commas should never have a space before them" 118 | regex: '\s+,' 119 | spaces_over_tabs: 120 | name: "Use (4) spaces instead of tabs" 121 | regex: '\t' 122 | xctestcase: 123 | name: "Use XCTestCase over XCTest to ensure tests run properly" 124 | regex: ': XCTest[\s,]+' 125 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | Copyright 2022-2025 The Connect Authors 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | 20 | 21 | -------------------------------------------------------------------------------- /Connect-Swift-Mocks.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Connect-Swift-Mocks' 3 | spec.module_name = 'ConnectMocks' 4 | spec.version = '1.0.3' 5 | spec.license = { :type => 'Apache 2.0', :file => 'LICENSE' } 6 | spec.summary = 'Mocks for testing with Connect-Swift.' 7 | spec.homepage = 'https://github.com/connectrpc/connect-swift' 8 | spec.author = 'The Connect Authors' 9 | spec.source = { :git => 'https://github.com/connectrpc/connect-swift.git', :tag => spec.version } 10 | 11 | spec.ios.deployment_target = '12.0' 12 | spec.osx.deployment_target = '10.15' 13 | spec.tvos.deployment_target = '13.0' 14 | spec.watchos.deployment_target = '6.0' 15 | 16 | spec.dependency 'Connect-Swift', "#{spec.version.to_s}" 17 | spec.dependency 'SwiftProtobuf', '~> 1.28.2' 18 | 19 | spec.source_files = 'Libraries/ConnectMocks/**/*.swift' 20 | 21 | spec.swift_versions = ['5.0'] 22 | end 23 | -------------------------------------------------------------------------------- /Connect-Swift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Connect-Swift' 3 | spec.module_name = 'Connect' 4 | spec.version = '1.0.3' 5 | spec.license = { :type => 'Apache 2.0', :file => 'LICENSE' } 6 | spec.summary = 'Idiomatic gRPC & Connect RPCs for Swift.' 7 | spec.homepage = 'https://github.com/connectrpc/connect-swift' 8 | spec.author = 'The Connect Authors' 9 | spec.source = { :git => 'https://github.com/connectrpc/connect-swift.git', :tag => spec.version } 10 | 11 | spec.ios.deployment_target = '12.0' 12 | spec.osx.deployment_target = '10.15' 13 | spec.tvos.deployment_target = '13.0' 14 | spec.watchos.deployment_target = '6.0' 15 | 16 | spec.dependency 'SwiftProtobuf', '~> 1.28.2' 17 | 18 | spec.source_files = 'Libraries/Connect/**/*.swift' 19 | 20 | spec.swift_versions = ['5.0'] 21 | end 22 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/ElizaCocoaPodsApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '15.0' 2 | 3 | target 'ElizaCocoaPodsApp' do 4 | use_frameworks! 5 | 6 | # For real projects, use a version instead of a path: 7 | # pod 'Connect-Swift', '~> x.y.z' 8 | pod 'Connect-Swift', :path => '../..' 9 | end 10 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Connect-Swift (1.0.3): 3 | - SwiftProtobuf (~> 1.28.2) 4 | - SwiftProtobuf (1.28.2) 5 | 6 | DEPENDENCIES: 7 | - Connect-Swift (from `../..`) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SwiftProtobuf 12 | 13 | EXTERNAL SOURCES: 14 | Connect-Swift: 15 | :path: "../.." 16 | 17 | SPEC CHECKSUMS: 18 | Connect-Swift: 537f3cb4148768054c7310ca0382c4a36011b4af 19 | SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d 20 | 21 | PODFILE CHECKSUM: b598f373a6ab5add976b09c2ac79029bf2200d48 22 | 23 | COCOAPODS: 1.15.2 24 | -------------------------------------------------------------------------------- /Examples/ElizaCocoaPodsApp/README.md: -------------------------------------------------------------------------------- 1 | # ElizaSwiftPackageApp example 2 | 3 | This example app imports the `Connect` library using CocoaPods, 4 | and provides an interface for 5 | [chatting with Eliza](https://connectrpc.com/demo). 6 | 7 | The app has support for chatting using a variety of protocols supported by 8 | the Connect library: 9 | 10 | - [Connect](https://connectrpc.com) + unary 11 | - [Connect](https://connectrpc.com) + streaming 12 | - [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) + unary 13 | - [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) + streaming 14 | 15 | **Note that vanilla gRPC support is not available in this example because 16 | [SwiftNIO does not support CocoaPods](https://github.com/apple/swift-nio/issues/2393).** 17 | 18 | ## Try it out 19 | 20 | 1. Ensure you have CocoaPods installed (`brew install cocoapods`) 21 | 2. `cd` into this directory and install the pods (`pod install`) 22 | 3. Open the generated `.xcworkspace` file (`xed .`) 23 | 4. Build the app target using Xcode 24 | 25 | Note that the [`Podfile`](./Podfile) uses a local path reference to the 26 | Connect library in this repository, rather than the one in the CocoaPods 27 | specs repo. 28 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/AppSources/ElizaApp.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | @main 18 | struct ElizaApp: App { 19 | var body: some Scene { 20 | WindowGroup { 21 | MenuView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/AppSources/Message.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | struct Message { 18 | enum Author { 19 | case eliza 20 | case user 21 | } 22 | 23 | let id = UUID() 24 | let message: String 25 | let author: Author 26 | } 27 | 28 | extension Message: Identifiable { 29 | typealias ID = UUID 30 | } 31 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/AppSources/MessagingView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import SwiftUI 17 | 18 | struct MessagingView: View { 19 | @State private var currentMessage = "" 20 | @ObservedObject private var viewModel: ViewModel 21 | 22 | @Environment(\.presentationMode) 23 | private var presentationMode 24 | 25 | init(viewModel: ViewModel) { 26 | self.viewModel = viewModel 27 | } 28 | 29 | var body: some View { 30 | VStack { 31 | ScrollViewReader { listView in 32 | // ScrollViewReader crashes in iOS 16 with ListView: 33 | // https://developer.apple.com/forums/thread/712510 34 | // Using ScrollView + ForEach as a workaround. 35 | ScrollView { 36 | ForEach(self.viewModel.messages) { message in 37 | VStack { 38 | switch message.author { 39 | case .user: 40 | HStack { 41 | Spacer() 42 | Text("You") 43 | .foregroundColor(.gray) 44 | .fontWeight(.semibold) 45 | } 46 | HStack { 47 | Spacer() 48 | Text(message.message) 49 | .multilineTextAlignment(.trailing) 50 | } 51 | case .eliza: 52 | HStack { 53 | Text("Eliza") 54 | .foregroundColor(.blue) 55 | .fontWeight(.semibold) 56 | Spacer() 57 | } 58 | HStack { 59 | Text(message.message) 60 | .multilineTextAlignment(.leading) 61 | Spacer() 62 | } 63 | } 64 | } 65 | .id(message.id) 66 | } 67 | } 68 | .onChange(of: self.viewModel.messages.count) { messageCount in 69 | listView.scrollTo(self.viewModel.messages[messageCount - 1].id) 70 | } 71 | } 72 | 73 | HStack { 74 | TextField("Write your message...", text: self.$currentMessage) 75 | .onSubmit { self.sendMessage() } 76 | .submitLabel(.send) 77 | Button("Send", action: { self.sendMessage() }) 78 | .foregroundColor(.blue) 79 | } 80 | } 81 | .padding() 82 | .navigationBarTitleDisplayMode(.inline) 83 | .navigationBarBackButtonHidden(true) 84 | .toolbar { 85 | ToolbarItem(placement: .navigationBarTrailing) { 86 | Button("End Chat") { 87 | self.viewModel.endChat() 88 | self.presentationMode.wrappedValue.dismiss() 89 | } 90 | } 91 | } 92 | } 93 | 94 | private func sendMessage() { 95 | let messageToSend = self.currentMessage 96 | if messageToSend.isEmpty { 97 | return 98 | } 99 | 100 | Task { await self.viewModel.send(messageToSend) } 101 | self.currentMessage = "" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/AppSources/MessagingViewModel.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Connect 17 | import os.log 18 | 19 | private typealias ConverseRequest = Connectrpc_Eliza_V1_ConverseRequest 20 | private typealias ConverseResponse = Connectrpc_Eliza_V1_ConverseResponse 21 | 22 | private typealias SayRequest = Connectrpc_Eliza_V1_SayRequest 23 | private typealias SayResponse = Connectrpc_Eliza_V1_SayResponse 24 | 25 | /// View model that can be injected into a `MessagingView`. 26 | @MainActor 27 | protocol MessagingViewModel: ObservableObject { 28 | /// The current set of messages. Observable by storing the view model as an `@ObservedObject`. 29 | var messages: [Message] { get } 30 | 31 | /// Send a message to the upstream service. 32 | /// This message and any responses will be appended to `messages`. 33 | /// 34 | /// - parameter message: The message to send. 35 | func send(_ message: String) async 36 | 37 | /// End the chat session (and close connections if needed). 38 | func endChat() 39 | } 40 | 41 | /// View model that uses unary requests for messaging. 42 | @MainActor 43 | final class UnaryMessagingViewModel: MessagingViewModel { 44 | private let client: Connectrpc_Eliza_V1_ElizaServiceClientInterface 45 | 46 | @Published private(set) var messages = [Message]() 47 | 48 | init(client: Connectrpc_Eliza_V1_ElizaServiceClientInterface) { 49 | self.client = client 50 | } 51 | 52 | func send(_ sentence: String) async { 53 | let request = SayRequest.with { $0.sentence = sentence } 54 | self.messages.append(Message(message: sentence, author: .user)) 55 | 56 | let response = await self.client.say(request: request, headers: [:]) 57 | os_log(.debug, "Eliza unary response: %@", String(describing: response)) 58 | self.messages.append(Message( 59 | message: response.message?.sentence ?? "No response", author: .eliza 60 | )) 61 | } 62 | 63 | func endChat() {} 64 | } 65 | 66 | /// View model that uses bidirectional streaming for messaging. 67 | @MainActor 68 | final class BidirectionalStreamingMessagingViewModel: MessagingViewModel { 69 | private let client: Connectrpc_Eliza_V1_ElizaServiceClientInterface 70 | private lazy var elizaStream = self.client.converse(headers: [:]) 71 | 72 | @Published private(set) var messages = [Message]() 73 | 74 | init(client: Connectrpc_Eliza_V1_ElizaServiceClientInterface) { 75 | self.client = client 76 | self.observeResponses() 77 | } 78 | 79 | func send(_ sentence: String) async { 80 | do { 81 | let request = ConverseRequest.with { $0.sentence = sentence } 82 | self.messages.append(Message(message: sentence, author: .user)) 83 | try self.elizaStream.send(request) 84 | } catch let error { 85 | os_log( 86 | .error, "Failed to write message to stream: %@", error.localizedDescription 87 | ) 88 | } 89 | } 90 | 91 | func endChat() { 92 | self.elizaStream.close() 93 | } 94 | 95 | private func observeResponses() { 96 | Task { 97 | for await result in self.elizaStream.results() { 98 | switch result { 99 | case .headers(let headers): 100 | os_log(.debug, "Eliza headers: %@", headers) 101 | 102 | case .message(let message): 103 | os_log(.debug, "Eliza message: %@", String(describing: message)) 104 | self.messages.append(Message(message: message.sentence, author: .eliza)) 105 | 106 | case .complete(_, let error, let trailers): 107 | os_log(.debug, "Eliza completed with trailers: %@", trailers ?? [:]) 108 | let sentence: String 109 | if let error = error { 110 | os_log(.error, "Eliza error: %@", error.localizedDescription) 111 | sentence = "[Error: \(error)]" 112 | } else { 113 | sentence = "[Conversation ended]" 114 | } 115 | self.messages.append(Message(message: sentence, author: .eliza)) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/GeneratedSources/connectrpc/eliza/v1/eliza.connect.swift: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-connect-swift. DO NOT EDIT. 2 | // 3 | // Source: connectrpc/eliza/v1/eliza.proto 4 | // 5 | 6 | import Connect 7 | import Foundation 8 | import SwiftProtobuf 9 | 10 | /// ElizaService provides a way to talk to Eliza, a port of the DOCTOR script 11 | /// for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at 12 | /// the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the 13 | /// superficiality of human-computer communication. DOCTOR simulates a 14 | /// psychotherapist, and is commonly found as an Easter egg in emacs 15 | /// distributions. 16 | internal protocol Connectrpc_Eliza_V1_ElizaServiceClientInterface: Sendable { 17 | 18 | /// Say is a unary RPC. Eliza responds to the prompt with a single sentence. 19 | @available(iOS 13, *) 20 | func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Connect.Headers) async -> ResponseMessage 21 | 22 | /// Converse is a bidirectional RPC. The caller may exchange multiple 23 | /// back-and-forth messages with Eliza over a long-lived connection. Eliza 24 | /// responds to each ConverseRequest with a ConverseResponse. 25 | @available(iOS 13, *) 26 | func `converse`(headers: Connect.Headers) -> any Connect.BidirectionalAsyncStreamInterface 27 | 28 | /// Introduce is a server streaming RPC. Given the caller's name, Eliza 29 | /// returns a stream of sentences to introduce itself. 30 | @available(iOS 13, *) 31 | func `introduce`(headers: Connect.Headers) -> any Connect.ServerOnlyAsyncStreamInterface 32 | } 33 | 34 | /// Concrete implementation of `Connectrpc_Eliza_V1_ElizaServiceClientInterface`. 35 | internal final class Connectrpc_Eliza_V1_ElizaServiceClient: Connectrpc_Eliza_V1_ElizaServiceClientInterface, Sendable { 36 | private let client: Connect.ProtocolClientInterface 37 | 38 | internal init(client: Connect.ProtocolClientInterface) { 39 | self.client = client 40 | } 41 | 42 | @available(iOS 13, *) 43 | internal func `say`(request: Connectrpc_Eliza_V1_SayRequest, headers: Connect.Headers = [:]) async -> ResponseMessage { 44 | return await self.client.unary(path: "/connectrpc.eliza.v1.ElizaService/Say", idempotencyLevel: .noSideEffects, request: request, headers: headers) 45 | } 46 | 47 | @available(iOS 13, *) 48 | internal func `converse`(headers: Connect.Headers = [:]) -> any Connect.BidirectionalAsyncStreamInterface { 49 | return self.client.bidirectionalStream(path: "/connectrpc.eliza.v1.ElizaService/Converse", headers: headers) 50 | } 51 | 52 | @available(iOS 13, *) 53 | internal func `introduce`(headers: Connect.Headers = [:]) -> any Connect.ServerOnlyAsyncStreamInterface { 54 | return self.client.serverOnlyStream(path: "/connectrpc.eliza.v1.ElizaService/Introduce", headers: headers) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Examples/ElizaSharedSources/README.md: -------------------------------------------------------------------------------- 1 | # ElizaSharedSources example 2 | 3 | This directory contains sources that are shared by the Eliza example apps. 4 | See the examples for more details and usage: 5 | 6 | - [ElizaCocoaPodsApp](../ElizaCocoaPodsApp) 7 | - [ElizaSwiftPackageApp](../ElizaSwiftPackageApp) 8 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-atomics", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-atomics.git", 7 | "state" : { 8 | "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", 9 | "version" : "1.1.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 18 | "version" : "1.1.4" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-nio", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-nio.git", 25 | "state" : { 26 | "revision" : "34d486b01cd891297ac615e40d5999536a1e138d", 27 | "version" : "2.83.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-nio-http2", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-nio-http2.git", 34 | "state" : { 35 | "revision" : "4281466512f63d1bd530e33f4aa6993ee7864be0", 36 | "version" : "1.36.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-nio-ssl", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-nio-ssl.git", 43 | "state" : { 44 | "revision" : "4b38f35946d00d8f6176fe58f96d83aba64b36c7", 45 | "version" : "2.31.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-protobuf", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-protobuf.git", 52 | "state" : { 53 | "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", 54 | "version" : "1.28.2" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-system", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-system.git", 61 | "state" : { 62 | "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", 63 | "version" : "1.4.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/ElizaSwiftPackageApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Examples/ElizaSwiftPackageApp/README.md: -------------------------------------------------------------------------------- 1 | # ElizaSwiftPackageApp example 2 | 3 | This example app imports the `Connect` library using Swift Package Manager, 4 | and provides an interface for 5 | [chatting with Eliza](https://buf.build/connectrpc/eliza). 6 | 7 | The app has support for chatting using a variety of protocols supported by 8 | the Connect library: 9 | 10 | - [Connect](https://connectrpc.com) + unary 11 | - [Connect](https://connectrpc.com) + streaming 12 | - [gRPC](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) + unary (using `ConnectGRPC` + `SwiftNIO`) 13 | - [gRPC](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) + streaming (using `ConnectGRPC` + `SwiftNIO`) 14 | - [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) + unary 15 | - [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) + streaming 16 | 17 | ## Try it out 18 | 19 | Simply open the `.xcodeproj` in this directory and build the app target 20 | using Xcode. 21 | 22 | Note that the project uses a local reference to the Connect package, 23 | rather than the GitHub URL. 24 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | # ConnectExamples 2 | 3 | This directory contains comprehensive example apps that utilize 4 | Connect-Swift and provide the ability to [chat with ELIZA][eliza-demo] over 5 | each supported protocol using either unary or streaming APIs: 6 | 7 | - [`ElizaSwiftPackageApp`](./ElizaSwiftPackageApp): Uses 8 | **Swift Package Manager**. 9 | - [`ElizaCocoaPodsApp`](./ElizaCocoaPodsApp): Uses **CocoaPods**. 10 | 11 | [eliza-demo]: https://connectrpc.com/demo 12 | -------------------------------------------------------------------------------- /Examples/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/apple/swift:v1.28.2 4 | opt: Visibility=Internal 5 | out: ./ElizaSharedSources/GeneratedSources 6 | - name: connect-swift 7 | opt: 8 | - GenerateServiceMetadata=false 9 | - Visibility=Internal 10 | out: ./ElizaSharedSources/GeneratedSources 11 | path: ../.tmp/bin/protoc-gen-connect-swift 12 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/GeneratedSources/proto/grpc/status/v1/status.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // swift-format-ignore-file 3 | // swiftlint:disable all 4 | // 5 | // Generated by the Swift generator plugin for the protocol buffer compiler. 6 | // Source: proto/grpc/status/v1/status.proto 7 | // 8 | // For information on using the generated types, please see the documentation: 9 | // https://github.com/apple/swift-protobuf/ 10 | 11 | // Copyright 2022-2025 The Connect Authors 12 | // 13 | // Licensed under the Apache License, Version 2.0 (the "License"); 14 | // you may not use this file except in compliance with the License. 15 | // You may obtain a copy of the License at 16 | // 17 | // http://www.apache.org/licenses/LICENSE-2.0 18 | // 19 | // Unless required by applicable law or agreed to in writing, software 20 | // distributed under the License is distributed on an "AS IS" BASIS, 21 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | // See the License for the specific language governing permissions and 23 | // limitations under the License. 24 | 25 | import SwiftProtobuf 26 | 27 | // If the compiler emits an error on this type, it is because this file 28 | // was generated by a version of the `protoc` Swift plug-in that is 29 | // incompatible with the version of SwiftProtobuf to which you are linking. 30 | // Please ensure that you are building against the same version of the API 31 | // that was used to generate this file. 32 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 33 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 34 | typealias Version = _2 35 | } 36 | 37 | /// See https://cloud.google.com/apis/design/errors. 38 | /// 39 | /// This struct must remain binary-compatible with 40 | /// https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto. 41 | struct Grpc_Status_V1_Status: Sendable { 42 | // SwiftProtobuf.Message conformance is added in an extension below. See the 43 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 44 | // methods supported on all messages. 45 | 46 | /// a google.rpc.Code 47 | var code: Int32 = 0 48 | 49 | /// developer-facing, English (localize in details or client-side) 50 | var message: String = String() 51 | 52 | var details: [SwiftProtobuf.Google_Protobuf_Any] = [] 53 | 54 | var unknownFields = SwiftProtobuf.UnknownStorage() 55 | 56 | init() {} 57 | } 58 | 59 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 60 | 61 | fileprivate let _protobuf_package = "grpc.status.v1" 62 | 63 | extension Grpc_Status_V1_Status: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 64 | static let protoMessageName: String = _protobuf_package + ".Status" 65 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 66 | 1: .same(proto: "code"), 67 | 2: .same(proto: "message"), 68 | 3: .same(proto: "details"), 69 | ] 70 | 71 | mutating func decodeMessage(decoder: inout D) throws { 72 | while let fieldNumber = try decoder.nextFieldNumber() { 73 | // The use of inline closures is to circumvent an issue where the compiler 74 | // allocates stack space for every case branch when no optimizations are 75 | // enabled. https://github.com/apple/swift-protobuf/issues/1034 76 | switch fieldNumber { 77 | case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() 78 | case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() 79 | case 3: try { try decoder.decodeRepeatedMessageField(value: &self.details) }() 80 | default: break 81 | } 82 | } 83 | } 84 | 85 | func traverse(visitor: inout V) throws { 86 | if self.code != 0 { 87 | try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) 88 | } 89 | if !self.message.isEmpty { 90 | try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) 91 | } 92 | if !self.details.isEmpty { 93 | try visitor.visitRepeatedMessageField(value: self.details, fieldNumber: 3) 94 | } 95 | try unknownFields.traverse(visitor: &visitor) 96 | } 97 | 98 | static func ==(lhs: Grpc_Status_V1_Status, rhs: Grpc_Status_V1_Status) -> Bool { 99 | if lhs.code != rhs.code {return false} 100 | if lhs.message != rhs.message {return false} 101 | if lhs.details != rhs.details {return false} 102 | if lhs.unknownFields != rhs.unknownFields {return false} 103 | return true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/BidirectionalAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Concrete **internal** implementation of `BidirectionalAsyncStreamInterface`. 18 | /// Provides the necessary wiring to bridge from closures/callbacks to Swift's `AsyncStream` 19 | /// to work with async/await. 20 | /// 21 | /// If the library removes callback support in favor of only supporting async/await in the future, 22 | /// this class can be simplified. 23 | @available(iOS 13, *) 24 | class BidirectionalAsyncStream< 25 | Input: ProtobufMessage, Output: ProtobufMessage 26 | >: @unchecked Sendable { 27 | /// The underlying async stream that will be exposed to the consumer. 28 | /// Force unwrapped because it captures `self` on `init`. 29 | private var asyncStream: AsyncStream>! 30 | /// Stored closure to provide access to the `AsyncStream.Continuation` so that result data 31 | /// can be passed through to the `AsyncStream` when received. 32 | /// Force unwrapped because it must be set within the context of the `AsyncStream.Continuation`. 33 | private var receiveResult: ((StreamResult) -> Void)! 34 | /// Callbacks used to send outbound data and close the stream. 35 | /// Optional because these callbacks are not available until the stream is initialized. 36 | private var requestCallbacks: RequestCallbacks? 37 | 38 | private struct NotConfiguredForSendingError: Swift.Error {} 39 | 40 | /// Initialize a new stream. 41 | /// 42 | /// Note: `configureForSending()` must be called before using the stream. 43 | init() { 44 | self.asyncStream = AsyncStream> { continuation in 45 | self.receiveResult = { result in 46 | if Task.isCancelled { 47 | return 48 | } 49 | switch result { 50 | case .headers, .message: 51 | continuation.yield(result) 52 | case .complete: 53 | continuation.yield(result) 54 | continuation.finish() 55 | } 56 | } 57 | continuation.onTermination = { @Sendable _ in 58 | self.requestCallbacks?.sendClose() 59 | } 60 | } 61 | } 62 | 63 | /// Enable sending data over this stream by providing a set of request callbacks to route data 64 | /// to the network client. Must be called before calling `send()`. 65 | /// 66 | /// - parameter requestCallbacks: Callbacks to use for sending request data and closing the 67 | /// stream. 68 | /// 69 | /// - returns: This instance of the stream (useful for chaining). 70 | @discardableResult 71 | func configureForSending(with requestCallbacks: RequestCallbacks) -> Self { 72 | self.requestCallbacks = requestCallbacks 73 | return self 74 | } 75 | 76 | /// Send a result to the consumer over the `results()` `AsyncStream`. 77 | /// Should be called by the protocol client when a result is received from the network. 78 | /// 79 | /// - parameter result: The new result that was received. 80 | func handleResultFromServer(_ result: StreamResult) { 81 | self.receiveResult(result) 82 | } 83 | } 84 | 85 | @available(iOS 13, *) 86 | extension BidirectionalAsyncStream: BidirectionalAsyncStreamInterface { 87 | @discardableResult 88 | func send(_ input: Input) throws -> Self { 89 | guard let sendData = self.requestCallbacks?.sendData else { 90 | throw NotConfiguredForSendingError() 91 | } 92 | 93 | sendData(input) 94 | return self 95 | } 96 | 97 | func results() -> AsyncStream> { 98 | return self.asyncStream 99 | } 100 | 101 | func close() { 102 | self.requestCallbacks?.sendClose() 103 | } 104 | 105 | func cancel() { 106 | self.requestCallbacks?.cancel() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/BidirectionalStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Concrete **internal** implementation of `BidirectionalStreamInterface`. 18 | final class BidirectionalStream: Sendable { 19 | private let requestCallbacks: RequestCallbacks 20 | 21 | init(requestCallbacks: RequestCallbacks) { 22 | self.requestCallbacks = requestCallbacks 23 | } 24 | } 25 | 26 | extension BidirectionalStream: BidirectionalStreamInterface { 27 | typealias Input = Message 28 | 29 | @discardableResult 30 | func send(_ input: Input) -> Self { 31 | self.requestCallbacks.sendData(input) 32 | return self 33 | } 34 | 35 | func close() { 36 | self.requestCallbacks.sendClose() 37 | } 38 | 39 | func cancel() { 40 | self.requestCallbacks.cancel() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ClientOnlyAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Concrete **internal** implementation of `ClientOnlyAsyncStreamInterface`. 18 | /// Provides the necessary wiring to bridge from closures/callbacks to Swift's `AsyncStream` 19 | /// to work with async/await. 20 | /// 21 | /// This subclasses `BidirectionalAsyncStream` since its behavior is purely additive (it overlays 22 | /// some additional validation) and both types are internal to the package, not public. 23 | @available(iOS 13, *) 24 | final class ClientOnlyAsyncStream< 25 | Input: ProtobufMessage, Output: ProtobufMessage 26 | >: BidirectionalAsyncStream, @unchecked Sendable { 27 | private let receivedResults = Locked([StreamResult]()) 28 | 29 | override func handleResultFromServer(_ result: StreamResult) { 30 | let (isComplete, results) = self.receivedResults.perform { results in 31 | results.append(result) 32 | if case .complete = result { 33 | return (true, ClientOnlyStreamValidation.validatedFinalClientStreamResults(results)) 34 | } else { 35 | return (false, []) 36 | } 37 | } 38 | guard isComplete else { 39 | return 40 | } 41 | results.forEach(super.handleResultFromServer) 42 | } 43 | } 44 | 45 | @available(iOS 13, *) 46 | extension ClientOnlyAsyncStream: ClientOnlyAsyncStreamInterface { 47 | func closeAndReceive() { 48 | self.close() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ClientOnlyStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Concrete **internal** implementation of `ClientOnlyStreamInterface`. 18 | /// 19 | /// The complexity around configuring callbacks on this type is an artifact of the library 20 | /// supporting both callbacks and async/await. This is internal to the package, and not public. 21 | final class ClientOnlyStream: @unchecked Sendable { 22 | private let onResult: @Sendable (StreamResult) -> Void 23 | private let receivedResults = Locked([StreamResult]()) 24 | /// Callbacks used to send outbound data and close the stream. 25 | /// Optional because these callbacks are not available until the stream is initialized. 26 | private var requestCallbacks: RequestCallbacks? 27 | 28 | private struct NotConfiguredForSendingError: Swift.Error {} 29 | 30 | init(onResult: @escaping @Sendable (StreamResult) -> Void) { 31 | self.onResult = onResult 32 | } 33 | 34 | /// Enable sending data over this stream by providing a set of request callbacks to route data 35 | /// to the network client. Must be called before calling `send()`. 36 | /// 37 | /// - parameter requestCallbacks: Callbacks to use for sending request data and closing the 38 | /// stream. 39 | /// 40 | /// - returns: This instance of the stream (useful for chaining). 41 | @discardableResult 42 | func configureForSending(with requestCallbacks: RequestCallbacks) -> Self { 43 | self.requestCallbacks = requestCallbacks 44 | return self 45 | } 46 | 47 | /// Send a result to the consumer after doing additional validations for client-only streams. 48 | /// Should be called by the protocol client when a result is received from the network. 49 | /// 50 | /// - parameter result: The new result that was received. 51 | func handleResultFromServer(_ result: StreamResult) { 52 | let (isComplete, results) = self.receivedResults.perform { results in 53 | results.append(result) 54 | if case .complete = result { 55 | return (true, ClientOnlyStreamValidation.validatedFinalClientStreamResults(results)) 56 | } else { 57 | return (false, []) 58 | } 59 | } 60 | guard isComplete else { 61 | return 62 | } 63 | results.forEach(self.onResult) 64 | } 65 | } 66 | 67 | extension ClientOnlyStream: ClientOnlyStreamInterface { 68 | @discardableResult 69 | func send(_ input: Input) throws -> Self { 70 | guard let sendData = self.requestCallbacks?.sendData else { 71 | throw NotConfiguredForSendingError() 72 | } 73 | 74 | sendData(input) 75 | return self 76 | } 77 | 78 | func closeAndReceive() { 79 | self.requestCallbacks?.sendClose() 80 | } 81 | 82 | func cancel() { 83 | self.requestCallbacks?.cancel() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ClientOnlyStreamValidation.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Namespace for performing client-only stream validation. 18 | enum ClientOnlyStreamValidation { 19 | /// Applies some validations which are only relevant for client-only streams. 20 | /// 21 | /// Should be called after all values have been received over a client stream. Since client 22 | /// streams only expect 1 result, all values returned from the server should be buffered before 23 | /// being validated here and returned to the caller. 24 | /// 25 | /// - parameter results: The buffered list of results to validate. 26 | /// 27 | /// - returns: The list of stream results which should be returned to the caller. 28 | static func validatedFinalClientStreamResults( 29 | _ results: [StreamResult] 30 | ) -> [StreamResult] { 31 | var messageCount = 0 32 | for result in results { 33 | switch result { 34 | case .headers: 35 | continue 36 | case .message: 37 | messageCount += 1 38 | case .complete(let code, _, _): 39 | if code != .ok { 40 | return results 41 | } 42 | } 43 | } 44 | 45 | if messageCount < 1 { 46 | return [ 47 | .complete( 48 | code: .internalError, error: ConnectError( 49 | code: .unimplemented, message: "unary stream has no messages" 50 | ), trailers: nil 51 | ), 52 | ] 53 | } else if messageCount > 1 { 54 | return [ 55 | .complete( 56 | code: .internalError, error: ConnectError( 57 | code: .unimplemented, message: "unary stream has multiple messages" 58 | ), trailers: nil 59 | ), 60 | ] 61 | } else { 62 | return results 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ConnectEndStreamResponse.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Structure modeling the final JSON message that is returned by Connect streams: 16 | /// https://connectrpc.com/docs/protocol#error-end-stream 17 | struct ConnectEndStreamResponse: Sendable { 18 | /// Connect error that was returned with the response. 19 | let error: ConnectError? 20 | /// Additional metadata that was passed with the response. Keys are guaranteed to be lowercased. 21 | let metadata: Trailers? 22 | } 23 | 24 | extension ConnectEndStreamResponse: Decodable { 25 | private enum CodingKeys: String, CodingKey { 26 | case error = "error" 27 | case metadata = "metadata" 28 | } 29 | 30 | init(from decoder: Decoder) throws { 31 | let container = try decoder.container(keyedBy: CodingKeys.self) 32 | let rawMetadata = try container.decodeIfPresent(Trailers.self, forKey: .metadata) 33 | self.init( 34 | error: try container.decodeIfPresent(ConnectError.self, forKey: .error), 35 | metadata: rawMetadata?.reduce(into: Trailers()) { trailers, current in 36 | trailers[current.key.lowercased()] = current.value 37 | } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ServerOnlyAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Concrete **internal** implementation of `ServerOnlyAsyncStreamInterface`. 18 | @available(iOS 13, *) 19 | final class ServerOnlyAsyncStream: Sendable { 20 | private let bidirectionalStream: BidirectionalAsyncStream 21 | 22 | init(bidirectionalStream: BidirectionalAsyncStream) { 23 | self.bidirectionalStream = bidirectionalStream 24 | } 25 | } 26 | 27 | @available(iOS 13, *) 28 | extension ServerOnlyAsyncStream: ServerOnlyAsyncStreamInterface { 29 | func send(_ input: Input) throws { 30 | try self.bidirectionalStream.send(input) 31 | self.bidirectionalStream.close() 32 | } 33 | 34 | func results() -> AsyncStream> { 35 | return self.bidirectionalStream.results() 36 | } 37 | 38 | func cancel() { 39 | self.bidirectionalStream.cancel() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/ServerOnlyStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Concrete **internal** implementation of `ServerOnlyStreamInterface`. 18 | final class ServerOnlyStream: Sendable { 19 | private let bidirectionalStream: BidirectionalStream 20 | 21 | init(bidirectionalStream: BidirectionalStream) { 22 | self.bidirectionalStream = bidirectionalStream 23 | } 24 | } 25 | 26 | extension ServerOnlyStream: ServerOnlyStreamInterface { 27 | typealias Input = Message 28 | 29 | func send(_ input: Message) { 30 | self.bidirectionalStream.send(input) 31 | self.bidirectionalStream.close() 32 | } 33 | 34 | func cancel() { 35 | self.bidirectionalStream.cancel() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Streaming/URLSessionStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Stream implementation that wraps a `URLSession` stream. 18 | /// 19 | /// Note: This class is `@unchecked Sendable` because the `Foundation.{Input|Output}Stream` 20 | /// types do not conform to `Sendable`. 21 | final class URLSessionStream: NSObject, @unchecked Sendable { 22 | private let closedByServer = Locked(false) 23 | private let readStream: Foundation.InputStream 24 | private let responseCallbacks: ResponseCallbacks 25 | private let task: URLSessionUploadTask 26 | private let writeStream: Foundation.OutputStream 27 | 28 | enum Error: Swift.Error { 29 | case unableToFindBaseAddress 30 | case unableToWriteData 31 | } 32 | 33 | var requestBodyStream: Foundation.InputStream { 34 | return self.readStream 35 | } 36 | 37 | var taskID: Int { 38 | return self.task.taskIdentifier 39 | } 40 | 41 | init( 42 | request: URLRequest, 43 | session: URLSession, 44 | responseCallbacks: ResponseCallbacks 45 | ) { 46 | var readStream: Foundation.InputStream! 47 | var writeStream: Foundation.OutputStream! 48 | Foundation.Stream.getBoundStreams( 49 | withBufferSize: 2 * (1_024 * 1_024), 50 | inputStream: &readStream, 51 | outputStream: &writeStream 52 | ) 53 | 54 | self.responseCallbacks = responseCallbacks 55 | self.readStream = readStream 56 | self.writeStream = writeStream 57 | self.task = session.uploadTask(withStreamedRequest: request) 58 | super.init() 59 | 60 | writeStream.schedule(in: .current, forMode: .default) 61 | writeStream.open() 62 | self.task.resume() 63 | } 64 | 65 | // MARK: - Outbound 66 | 67 | func sendData(_ data: Data) throws { 68 | var remaining = Data(data) 69 | while !remaining.isEmpty { 70 | let bytesWritten = try remaining.withUnsafeBytes { pointer -> Int in 71 | guard let baseAddress = pointer.baseAddress else { 72 | throw Error.unableToFindBaseAddress 73 | } 74 | 75 | return self.writeStream.write( 76 | baseAddress.assumingMemoryBound(to: UInt8.self), 77 | maxLength: remaining.count 78 | ) 79 | } 80 | 81 | if bytesWritten >= 0 { 82 | remaining = remaining.dropFirst(bytesWritten) 83 | } else { 84 | throw Error.unableToWriteData 85 | } 86 | } 87 | } 88 | 89 | func cancel() { 90 | self.task.cancel() 91 | } 92 | 93 | func close() { 94 | self.writeStream.close() 95 | } 96 | 97 | // MARK: - Inbound 98 | 99 | func handleResponse(_ response: HTTPURLResponse) { 100 | let code = Code.fromURLSessionCode(response.statusCode) 101 | self.responseCallbacks.receiveResponseHeaders(response.formattedLowercasedHeaders()) 102 | if code != .ok { 103 | self.closedByServer.value = true 104 | self.responseCallbacks.receiveClose(code, [:], nil) 105 | } 106 | } 107 | 108 | func handleResponseData(_ data: Data) { 109 | if !self.closedByServer.value { 110 | self.responseCallbacks.receiveResponseData(data) 111 | } 112 | } 113 | 114 | func handleCompletion(error: Swift.Error?) { 115 | if self.closedByServer.value { 116 | return 117 | } 118 | 119 | self.closedByServer.value = true 120 | if let error = error { 121 | let code = Code.fromURLSessionCode((error as NSError).code) 122 | self.responseCallbacks.receiveClose( 123 | code, 124 | [:], 125 | ConnectError(code: code, message: error.localizedDescription) 126 | ) 127 | } else { 128 | self.responseCallbacks.receiveClose(.ok, [:], nil) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Unary/UnaryAsyncWrapper.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import os.log 16 | import SwiftProtobuf 17 | 18 | /// Internal actor used to wrap closure-based unary API calls in a way that allows them 19 | /// to be used with async/await while properly supporting cancelation. 20 | /// 21 | /// For discussions on why this is necessary, see: 22 | /// https://forums.swift.org/t/how-to-use-withtaskcancellationhandler-properly/54341/37 23 | /// https://stackoverflow.com/q/71898080 24 | @available(iOS 13, *) 25 | actor UnaryAsyncWrapper { 26 | private var cancelable: Cancelable? 27 | private let sendUnary: PerformClosure 28 | 29 | /// Accepts a closure to be called upon completion of a request and returns a cancelable which, 30 | /// when invoked, will cancel the underlying request. 31 | typealias PerformClosure = @Sendable ( 32 | @escaping @Sendable (ResponseMessage) -> Void 33 | ) -> Cancelable 34 | 35 | init(sendUnary: @escaping PerformClosure) { 36 | self.sendUnary = sendUnary 37 | } 38 | 39 | /// Provides an `async` interface for performing the `sendUnary` request. 40 | /// Canceling the request in the context of an async `Task` will appropriately cancel the 41 | /// outbound request and return a response with a `.canceled` status code. 42 | /// 43 | /// - returns: The response/result of the request. 44 | func send() async -> ResponseMessage { 45 | await withTaskCancellationHandler { 46 | await withCheckedContinuation { continuation in 47 | guard !Task.isCancelled else { 48 | continuation.resume( 49 | returning: ResponseMessage( 50 | code: .canceled, 51 | result: .failure(.canceled()) 52 | ) 53 | ) 54 | return 55 | } 56 | 57 | let hasResumed = Locked(false) 58 | self.cancelable = self.sendUnary { response in 59 | // In some circumstances where a request timeout and a server 60 | // error occur at nearly the same moment, the underlying 61 | // `swift-nio` system will trigger this callback twice. This check 62 | // discards the second occurrence to avoid resuming `continuation` 63 | // multiple times, which would result in a crash. 64 | guard !hasResumed.value else { 65 | os_log( 66 | .fault, 67 | """ 68 | `sendUnary` received duplicate callback and \ 69 | attempted to resume its continuation twice. 70 | """ 71 | ) 72 | return 73 | } 74 | continuation.resume(returning: response) 75 | hasResumed.perform(action: { $0 = true }) 76 | } 77 | } 78 | } onCancel: { 79 | // When `Task.cancel` signals for this function to be canceled, 80 | // the underlying function will be canceled as well. 81 | Task(priority: .high) { 82 | await self.cancelable?.cancel() 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Utilities/Lock.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Internal implementation of a lock. Wraps usage of `os_unfair_lock`. 18 | final class Lock: @unchecked Sendable { 19 | private let underlyingLock: UnsafeMutablePointer 20 | 21 | init() { 22 | // Reasoning for allocating here: http://www.russbishop.net/the-law 23 | // When iOS 15 support is dropped, `OSAllocatedUnfairLock` should be used. 24 | self.underlyingLock = .allocate(capacity: 1) 25 | self.underlyingLock.initialize(to: os_unfair_lock()) 26 | } 27 | 28 | deinit { 29 | self.underlyingLock.deinitialize(count: 1) 30 | self.underlyingLock.deallocate() 31 | } 32 | 33 | /// Perform an action within the context of the lock. 34 | /// 35 | /// - parameter action: Closure to be executed in the context of the lock. 36 | /// 37 | /// - returns: The result of the closure. 38 | func perform(action: @escaping () -> T) -> T { 39 | os_unfair_lock_lock(self.underlyingLock) 40 | defer { os_unfair_lock_unlock(self.underlyingLock) } 41 | return action() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Utilities/Locked.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Class containing an internal lock which can be used to ensure thread-safe access to an 18 | /// underlying value. Conforms to `Sendable`, making it accessible from `@Sendable` closures. 19 | final class Locked: @unchecked Sendable { 20 | private let lock = Lock() 21 | private var wrappedValue: Wrapped 22 | 23 | /// Thread-safe access to the underlying value. 24 | var value: Wrapped { 25 | get { self.lock.perform { self.wrappedValue } } 26 | set { self.lock.perform { self.wrappedValue = newValue } } 27 | } 28 | 29 | /// Perform an action with the underlying value, potentially updating that value. 30 | /// 31 | /// - parameter action: Closure to perform with the underlying value. 32 | /// 33 | /// - returns: The value returned by the closure. 34 | @discardableResult 35 | func perform(action: @escaping (inout Wrapped) -> Result) -> Result { 36 | return self.lock.perform { 37 | action(&self.wrappedValue) 38 | } 39 | } 40 | 41 | init(_ value: Wrapped) { 42 | self.wrappedValue = value 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Libraries/Connect/Internal/Utilities/TimeoutTimer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | final class TimeoutTimer: @unchecked Sendable { 18 | private var hasTimedOut = false 19 | private var onTimeout: (() -> Void)? 20 | private let queue = DispatchQueue(label: "connectrpc.Timeout") 21 | private let timeout: TimeInterval 22 | private var workItem: DispatchWorkItem! // Force-unwrapped to allow capturing self in init 23 | 24 | var timedOut: Bool { 25 | return self.queue.sync { self.hasTimedOut } 26 | } 27 | 28 | init?(config: ProtocolClientConfig) { 29 | guard let timeout = config.timeout else { 30 | return nil 31 | } 32 | 33 | self.timeout = timeout 34 | self.workItem = DispatchWorkItem { [weak self] in 35 | self?.hasTimedOut = true 36 | self?.onTimeout?() 37 | } 38 | } 39 | 40 | deinit { 41 | self.cancel() 42 | } 43 | 44 | func start(onTimeout: @escaping () -> Void) { 45 | let milliseconds = Int(self.timeout * 1_000) 46 | self.queue.sync { self.onTimeout = onTimeout } 47 | self.queue.asyncAfter( 48 | deadline: .now() + .milliseconds(milliseconds), execute: self.workItem 49 | ) 50 | } 51 | 52 | func cancel() { 53 | self.queue.sync { 54 | self.workItem.cancel() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Libraries/Connect/PackageInternal/Headers+GRPC.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | extension Headers { 18 | /// **This should not be considered part of Connect's public/stable interface, and is subject 19 | /// to change. When the compiler supports it, this should be package-internal.** 20 | /// 21 | /// Adds required headers to gRPC and gRPC-Web requests/streams. 22 | /// 23 | /// - parameter config: The configuration to use for adding headers (i.e., for compression 24 | /// headers). 25 | /// - parameter grpcWeb: Should be true if using gRPC-Web, false if gRPC. 26 | /// 27 | /// - returns: A set of updated headers. 28 | @available( 29 | swift, 30 | deprecated: 100.0, 31 | message: "This is an internal-only API which will be made package-private in Swift 6." 32 | ) 33 | public func _addingGRPCHeaders(using config: ProtocolClientConfig, grpcWeb: Bool) -> Self { 34 | var headers = self 35 | headers[HeaderConstants.grpcAcceptEncoding] = config 36 | .acceptCompressionPoolNames() 37 | headers[HeaderConstants.grpcContentEncoding] = config.requestCompression 38 | .map { [$0.pool.name()] } 39 | if let timeout = config.timeout { 40 | headers[HeaderConstants.grpcTimeout] = ["\(Int(timeout * 1_000))m"] 41 | } 42 | if grpcWeb { 43 | headers[HeaderConstants.contentType] = [ 44 | "application/grpc-web+\(config.codec.name())", 45 | ] 46 | } else { 47 | headers[HeaderConstants.contentType] = [ 48 | "application/grpc+\(config.codec.name())", 49 | ] 50 | headers[HeaderConstants.grpcTE] = ["trailers"] 51 | } 52 | 53 | // Note that we do not comply with the recommended structure for user-agent: 54 | // https://github.com/grpc/grpc/blob/v1.51.1/doc/PROTOCOL-HTTP2.md#user-agents 55 | // But this behavior matches connect-web: 56 | // https://github.com/bufbuild/connect-web/blob/v0.4.0/packages/connect-core/src/grpc-web-create-request-header.ts#L33-L36 57 | // swiftlint:disable:previous line_length 58 | headers[HeaderConstants.xUserAgent] = ["@connectrpc/connect-swift"] 59 | return headers 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Libraries/Connect/PackageInternal/Trailers+gRPC.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension Trailers { 16 | /// **This should not be considered part of Connect's public/stable interface, and is subject 17 | /// to change. When the compiler supports it, this should be package-internal.** 18 | /// 19 | /// Identifies the status code from gRPC and gRPC-Web trailers. 20 | /// 21 | /// - returns: The gRPC status code, if specified. 22 | @available( 23 | swift, 24 | deprecated: 100.0, 25 | message: "This is an internal-only API which will be made package-private in Swift 6." 26 | ) 27 | public func _grpcStatus() -> Code? { 28 | return self[HeaderConstants.grpcStatus]? 29 | .first 30 | .flatMap(Int.init) 31 | .flatMap { Code(rawValue: $0) } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Implementation/Codecs/JSONCodec.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import SwiftProtobuf 17 | 18 | /// Codec providing functionality for serializing to/from JSON. 19 | public struct JSONCodec: Sendable { 20 | private let defaultEncodingOptions: JSONEncodingOptions 21 | private let deterministicEncodingOptions: JSONEncodingOptions 22 | private let decodingOptions: JSONDecodingOptions = { 23 | var options = JSONDecodingOptions() 24 | options.ignoreUnknownFields = true 25 | return options 26 | }() 27 | 28 | /// Designated initializer. 29 | /// 30 | /// - parameter alwaysEncodeEnumsAsInts: Always encode enums as ints. By default they are 31 | /// encoded as strings. 32 | /// - parameter preserveProtobufFieldNames: Whether to preserve Protobuf field names as they're 33 | /// defined in the `.proto` files. By default they are 34 | /// converted to Protobuf's JSON lowerCamelCase format. 35 | public init(alwaysEncodeEnumsAsInts: Bool = false, preserveProtobufFieldNames: Bool = false) { 36 | self.defaultEncodingOptions = { 37 | var encodingOptions = JSONEncodingOptions() 38 | encodingOptions.alwaysPrintEnumsAsInts = alwaysEncodeEnumsAsInts 39 | encodingOptions.preserveProtoFieldNames = preserveProtobufFieldNames 40 | return encodingOptions 41 | }() 42 | self.deterministicEncodingOptions = { 43 | var encodingOptions = JSONEncodingOptions() 44 | encodingOptions.useDeterministicOrdering = true 45 | encodingOptions.alwaysPrintEnumsAsInts = alwaysEncodeEnumsAsInts 46 | encodingOptions.preserveProtoFieldNames = preserveProtobufFieldNames 47 | return encodingOptions 48 | }() 49 | } 50 | } 51 | 52 | extension JSONCodec: Codec { 53 | public func name() -> String { 54 | return "json" 55 | } 56 | 57 | public func serialize(message: Input) throws -> Data { 58 | return try message.jsonUTF8Data(options: self.defaultEncodingOptions) 59 | } 60 | 61 | public func deterministicallySerialize(message: Input) throws -> Data { 62 | return try message.jsonUTF8Data(options: self.deterministicEncodingOptions) 63 | } 64 | 65 | public func deserialize(source: Data) throws -> Output { 66 | return try Output(jsonUTF8Data: source, options: self.decodingOptions) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Implementation/Codecs/ProtoCodec.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import SwiftProtobuf 17 | 18 | /// Codec providing functionality for serializing to/from Protobuf binary. 19 | public struct ProtoCodec { 20 | private let deterministicEncodingOptions: BinaryEncodingOptions = { 21 | var encodingOptions = BinaryEncodingOptions() 22 | encodingOptions.useDeterministicOrdering = true 23 | return encodingOptions 24 | }() 25 | 26 | public init() {} 27 | } 28 | 29 | extension ProtoCodec: Codec { 30 | public func name() -> String { 31 | return "proto" 32 | } 33 | 34 | public func serialize(message: Input) throws -> Data { 35 | return try message.serializedData() 36 | } 37 | 38 | public func deterministicallySerialize(message: Input) throws -> Data { 39 | return try message.serializedData(options: self.deterministicEncodingOptions) 40 | } 41 | 42 | public func deserialize(source: Data) throws -> Output { 43 | return try Output(serializedBytes: source) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Cancelable.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Type that wraps an action that can be canceled. 16 | public struct Cancelable: Sendable { 17 | /// Cancel the current action. 18 | public let cancel: @Sendable () -> Void 19 | 20 | public init(cancel: @escaping @Sendable () -> Void) { 21 | self.cancel = cancel 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Code.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Indicates a status of an RPC. 16 | /// The zero code in gRPC is OK, which indicates that the operation was a success. 17 | public enum Code: Int, CaseIterable, Equatable, Sendable { 18 | case ok = 0 19 | case canceled = 1 20 | case unknown = 2 21 | case invalidArgument = 3 22 | case deadlineExceeded = 4 23 | case notFound = 5 24 | case alreadyExists = 6 25 | case permissionDenied = 7 26 | case resourceExhausted = 8 27 | case failedPrecondition = 9 28 | case aborted = 10 29 | case outOfRange = 11 30 | case unimplemented = 12 31 | case internalError = 13 32 | case unavailable = 14 33 | case dataLoss = 15 34 | case unauthenticated = 16 35 | 36 | public var name: String { 37 | switch self { 38 | case .ok: 39 | return "ok" 40 | case .canceled: 41 | return "canceled" 42 | case .unknown: 43 | return "unknown" 44 | case .invalidArgument: 45 | return "invalid_argument" 46 | case .deadlineExceeded: 47 | return "deadline_exceeded" 48 | case .notFound: 49 | return "not_found" 50 | case .alreadyExists: 51 | return "already_exists" 52 | case .permissionDenied: 53 | return "permission_denied" 54 | case .resourceExhausted: 55 | return "resource_exhausted" 56 | case .failedPrecondition: 57 | return "failed_precondition" 58 | case .aborted: 59 | return "aborted" 60 | case .outOfRange: 61 | return "out_of_range" 62 | case .unimplemented: 63 | return "unimplemented" 64 | case .internalError: 65 | return "internal" 66 | case .unavailable: 67 | return "unavailable" 68 | case .dataLoss: 69 | return "data_loss" 70 | case .unauthenticated: 71 | return "unauthenticated" 72 | } 73 | } 74 | 75 | public static func fromHTTPStatus(_ status: Int) -> Self { 76 | // https://connectrpc.com/docs/protocol#http-to-error-code 77 | switch status { 78 | case 200: 79 | return .ok 80 | case 400: 81 | return .internalError 82 | case 401: 83 | return .unauthenticated 84 | case 403: 85 | return .permissionDenied 86 | case 404: 87 | return .unimplemented 88 | case 429, 502, 503, 504: 89 | return .unavailable 90 | default: 91 | return .unknown 92 | } 93 | } 94 | 95 | public static func fromName(_ name: String) -> Self? { 96 | return Self.allCases.first { $0.name == name } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Codec.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import SwiftProtobuf 17 | 18 | /// Defines a type that is capable of encoding and decoding messages using a specific format. 19 | public protocol Codec: Sendable { 20 | /// - returns: The name of the codec's format (e.g., "json", "protobuf"). Usually consumed 21 | /// in the form of adding the `content-type` header via "application/{name}". 22 | func name() -> String 23 | 24 | /// Serializes the input message into the codec's format. 25 | /// 26 | /// - parameter message: Typed input message. 27 | /// 28 | /// - returns: Serialized data that can be transmitted. 29 | func serialize(message: Input) throws -> Data 30 | 31 | /// Determininstically serializes the input message into the codec's format. 32 | /// Invocations of this function using the same version of the library with the same message 33 | /// are guranteed to produce identical outputs. This is less efficient than 34 | /// nondeterministic serialization, as it may result in performing sorts on map fields. 35 | /// 36 | /// Note that the deterministic serialization is NOT canonical across languages. 37 | /// It is NOT guaranteed to remain stable over time. It is unstable across 38 | /// different builds with schema changes due to unknown fields. Users who need 39 | /// canonical serialization (e.g., persistent storage in a canonical form, 40 | /// fingerprinting, etc.) should define their own canonicalization specification 41 | /// and implement their own serializer rather than relying on this API. 42 | /// 43 | /// - parameter message: Typed input message. 44 | /// 45 | /// - returns: Serialized data that can be transmitted. 46 | func deterministicallySerialize(message: Input) throws -> Data 47 | 48 | /// Deserializes data in the codec's format into a typed message. 49 | /// 50 | /// - parameter source: The source data to deserialize. 51 | /// 52 | /// - returns: The typed output message. 53 | func deserialize(source: Data) throws -> Output 54 | } 55 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/CompressionPool.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Conforming types provide the functionality to compress/decompress data using a specific 18 | /// algorithm. 19 | /// 20 | /// `ProtocolClientInterface` implementations are expected to use the first compression pool with 21 | /// a matching `name()` for decompressing inbound responses. 22 | /// 23 | /// Outbound request compression can be specified using additional options that specify a 24 | /// `compressionName` that matches a compression pool's `name()`. 25 | public protocol CompressionPool: Sendable { 26 | /// The name of the compression pool, which corresponds to the `content-encoding` header. 27 | /// Example: `gzip`. 28 | /// 29 | /// - returns: The name of the compression pool that can be used with the `content-encoding` 30 | /// header. 31 | func name() -> String 32 | 33 | /// Compress an outbound request message. 34 | /// 35 | /// - parameter data: The uncompressed request message. 36 | /// 37 | /// - returns: The compressed request message. 38 | func compress(data: Data) throws -> Data 39 | 40 | /// Decompress an inbound response message. 41 | /// 42 | /// - parameter data: The compressed response message. 43 | /// 44 | /// - returns: The uncompressed response message. 45 | func decompress(data: Data) throws -> Data 46 | } 47 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HTTPClientInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Interface for a client that performs underlying HTTP requests and streams with primitive types. 18 | public protocol HTTPClientInterface: Sendable { 19 | /// Perform a unary HTTP request. 20 | /// 21 | /// - parameter request: The outbound request headers and data. 22 | /// - parameter onMetrics: Closure that should be called when metrics are finalized. This may be 23 | /// called before or after `onResponse`. 24 | /// - parameter onResponse: Closure that should be called when a response is received. 25 | /// 26 | /// - returns: A type which can be used to cancel the outbound request. 27 | @discardableResult 28 | func unary( 29 | request: HTTPRequest, 30 | onMetrics: @escaping @Sendable (HTTPMetrics) -> Void, 31 | onResponse: @escaping @Sendable (HTTPResponse) -> Void 32 | ) -> Cancelable 33 | 34 | /// Initialize a new HTTP stream. 35 | /// 36 | /// - parameter request: The request headers to use for starting the stream. 37 | /// - parameter responseCallbacks: Set of callbacks that should be invoked by the HTTP client 38 | /// when response data is received from the server. 39 | /// 40 | /// - returns: Set of callbacks which can be called to send data over the stream or to close it. 41 | func stream( 42 | request: HTTPRequest, 43 | responseCallbacks: ResponseCallbacks 44 | ) -> RequestCallbacks 45 | } 46 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// HTTP methods supported by the library. 16 | public enum HTTPMethod: String, Sendable { 17 | case get = "GET" 18 | case post = "POST" 19 | } 20 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HTTPMetrics.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Contains metrics collected during the span of an HTTP request. 18 | public struct HTTPMetrics: Sendable { 19 | public let taskMetrics: URLSessionTaskMetrics? 20 | 21 | public init(taskMetrics: URLSessionTaskMetrics?) { 22 | self.taskMetrics = taskMetrics 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Request used for sending data to the server. 18 | public struct HTTPRequest: Sendable { 19 | /// Target URL for the request. 20 | public let url: URL 21 | /// Additional outbound headers for the request. 22 | public let headers: Headers 23 | /// Data to send with the request. 24 | public let message: Input 25 | /// HTTP method to use for the request. 26 | public let method: HTTPMethod 27 | /// Outbound trailers for the request. 28 | public let trailers: Trailers? 29 | /// Idempotency level of the request. 30 | public let idempotencyLevel: IdempotencyLevel 31 | 32 | public init( 33 | url: URL, headers: Headers, message: Input, method: HTTPMethod, 34 | trailers: Trailers?, idempotencyLevel: IdempotencyLevel 35 | ) { 36 | self.url = url 37 | self.headers = headers 38 | self.message = message 39 | self.method = method 40 | self.trailers = trailers 41 | self.idempotencyLevel = idempotencyLevel 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Unary HTTP response received from the server. 18 | public struct HTTPResponse: Sendable { 19 | /// The status code of the response. 20 | /// See https://connectrpc.com/docs/protocol/#error-codes for more info. 21 | public let code: Code 22 | /// Response headers specified by the server. 23 | public let headers: Headers 24 | /// Body data provided by the server. 25 | public let message: Data? 26 | /// Trailers provided by the server. 27 | public let trailers: Trailers 28 | /// The accompanying error, if the request failed. 29 | public let error: Swift.Error? 30 | /// Tracing information that can be used for logging or debugging network-level details. 31 | /// This information is expected to change when switching protocols (i.e., from Connect to 32 | /// gRPC-Web), as each protocol has different HTTP semantics. 33 | /// Nil in cases where no response was received from the server. 34 | public let tracingInfo: TracingInfo? 35 | 36 | public struct TracingInfo: Equatable, Sendable { 37 | /// HTTP status received from the server. 38 | public let httpStatus: Int 39 | 40 | public init(httpStatus: Int) { 41 | self.httpStatus = httpStatus 42 | } 43 | } 44 | 45 | public init( 46 | code: Code, headers: Headers, message: Data?, 47 | trailers: Trailers, error: Swift.Error?, tracingInfo: TracingInfo? 48 | ) { 49 | self.code = code 50 | self.headers = headers 51 | self.message = message 52 | self.trailers = trailers 53 | self.error = error 54 | self.tracingInfo = tracingInfo 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/HeaderConstants.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | public enum HeaderConstants { 16 | public static let acceptEncoding = "accept-encoding" 17 | public static let contentEncoding = "content-encoding" 18 | public static let contentType = "content-type" 19 | 20 | public static let connectProtocolVersion = "connect-protocol-version" 21 | public static let connectTimeoutMs = "connect-timeout-ms" 22 | 23 | public static let connectStreamingAcceptEncoding = "connect-accept-encoding" 24 | public static let connectStreamingContentEncoding = "connect-content-encoding" 25 | 26 | public static let xUserAgent = "x-user-agent" 27 | 28 | public static let grpcAcceptEncoding = "grpc-accept-encoding" 29 | public static let grpcContentEncoding = "grpc-encoding" 30 | public static let grpcMessage = "grpc-message" 31 | public static let grpcStatus = "grpc-status" 32 | public static let grpcStatusDetails = "grpc-status-details-bin" 33 | public static let grpcTimeout = "grpc-timeout" 34 | public static let grpcTE = "te" 35 | } 36 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Headers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Request/response headers. 16 | /// All keys are expected to be lowercased. 17 | /// Comma-separated values are split into individual items in the array. For example: 18 | /// On the wire: `accept-encoding: gzip,brotli` or `accept-encoding: gzip, brotli` 19 | /// Yields: `["accept-encoding": ["gzip", "brotli"]]` 20 | public typealias Headers = [String: [String]] 21 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/IdempotencyLevel.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | public enum IdempotencyLevel: Sendable { 16 | /// The default idempotency level. A procedure with 17 | /// this idempotency level may not be idempotent. This is appropriate for any kind of procedure. 18 | case unknown 19 | /// The idempotency level that specifies that a 20 | /// given call has no side-effects. This is equivalent to [RFC 9110 § 9.2.1] 21 | /// "safe" methods in terms of semantics. This procedure should not mutate 22 | /// any state. This idempotency level is appropriate for queries, or anything 23 | /// that would be suitable for an HTTP GET request. In addition, due to the 24 | /// lack of side-effects, such a procedure would be suitable to retry and 25 | /// expect that the results will not be altered by preceding attempts. 26 | /// 27 | /// [RFC 9110 § 9.2.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.1 28 | case noSideEffects 29 | /// The idempotency level that specifies that a 30 | /// given call is "idempotent", such that multiple instances of the same 31 | /// request to this procedure would have the same side-effects as a single 32 | /// request. This is equivalent to [RFC 9110 § 9.2.2] "idempotent" methods. 33 | /// This level is a subset of the previous level. This idempotency level is 34 | /// appropriate for any procedure that is safe to retry multiple times 35 | /// and be guaranteed that the response and side-effects will not be altered 36 | /// as a result of multiple attempts, for example, entity deletion requests. 37 | /// 38 | /// [RFC 9110 § 9.2.2]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2 39 | case idempotent 40 | } 41 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Interceptors/Interceptor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Interceptors are a powerful way to observe and mutate outbound and inbound 16 | /// headers, data, trailers, typed messages, and errors both for unary APIs and streams. 17 | /// 18 | /// Each interceptor is instantiated **once per request or stream** and 19 | /// provides a set of functions that are invoked by the client during the lifecycle 20 | /// of that call. Each function allows the interceptor to observe and store 21 | /// state, as well as to mutate outbound or inbound content. Interceptors have the ability to 22 | /// interact with both typed messages (request messages prior to serialization and response 23 | /// messages after deserialization) and raw data. 24 | /// 25 | /// Every interceptor has the opportunity to perform asynchronous work before passing a potentially 26 | /// altered value to the next interceptor in the chain. When the end of the chain is reached, the 27 | /// final value is passed to the networking client, where it is sent to the server 28 | /// (outbound request) or to the caller (inbound response). 29 | /// 30 | /// Interceptors may also fail outbound requests before they are sent; subsequent 31 | /// interceptors in the chain will not be invoked, and the error will be returned to the 32 | /// original caller. 33 | /// 34 | /// Interceptors are invoked in FIFO order on the request path, and in LIFO order on the 35 | /// response path. For example: 36 | /// 37 | /// Client -> A -> B -> C -> D -> Server 38 | /// Client <- D <- C <- B <- A <- Server 39 | /// 40 | /// Interceptors receive both the current value and a closure that 41 | /// should be called to resume the interceptor chain. Propagation will not continue until 42 | /// this closure is invoked. Additional values may still be passed to a given interceptor even 43 | /// though it has not yet continued the chain with a previous value. For example: 44 | /// 45 | /// 1. A request is sent. 46 | /// 2. Response headers are received, and an interceptor pauses the chain while processing them. 47 | /// 3. The first chunk of streamed response data is received, and the interceptor is invoked with 48 | /// this value. 49 | /// 4. The interceptor is expected to resume with headers first, and then with data after. 50 | /// 51 | /// Implementations should be thread-safe (hence the `Sendable` requirements), 52 | /// as functions can be invoked from different threads during the span of a request or 53 | /// stream due to the asynchronous nature of other interceptors which may be present in the chain. 54 | /// 55 | /// This high-level protocol encompasses characteristics shared by unary and stream interceptors. 56 | /// Implementations can interact with unary requests, streams, or both. See the 57 | /// derived `UnaryInterceptor` and `StreamInterceptor` protocols for additional details. 58 | public protocol Interceptor: AnyObject, Sendable { 59 | /// Observe and/or mutate response metrics for a unary request or stream. 60 | /// 61 | /// - parameter metrics: Metrics containing data about the completed request/stream. 62 | /// - parameter proceed: Closure which must be called to pass (potentially altered) data to the 63 | /// next interceptor. 64 | @Sendable 65 | func handleResponseMetrics( 66 | _ metrics: HTTPMetrics, 67 | proceed: @escaping @Sendable (HTTPMetrics) -> Void 68 | ) 69 | } 70 | 71 | extension Interceptor { 72 | @Sendable 73 | public func handleResponseMetrics( 74 | _ metrics: HTTPMetrics, 75 | proceed: @escaping @Sendable (HTTPMetrics) -> Void 76 | ) { 77 | proceed(metrics) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Interceptors/InterceptorFactory.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Factory for creating interceptors. Invoked once per request/stream to produce interceptor 16 | /// instances. 17 | /// 18 | /// This wrapper also captures the underlying type of the interceptor class, allowing the factory 19 | /// to only instantiate instances when necessary (for example, a stream-only interceptor should not 20 | /// be instantiated for a unary request). 21 | public struct InterceptorFactory: Sendable { 22 | private let factory: @Sendable (ProtocolClientConfig) -> Interceptor 23 | private let interceptorType: Interceptor.Type 24 | 25 | /// Initialize a new factory which may be used to produce instances of an interceptor type. 26 | /// 27 | /// - parameter factory: Closure to use to produce a new interceptor instance given a config. 28 | public init(factory: @escaping @Sendable (ProtocolClientConfig) -> T) { 29 | self.factory = factory 30 | self.interceptorType = T.self 31 | } 32 | 33 | // MARK: - Internal 34 | 35 | func createUnary(with config: ProtocolClientConfig) -> UnaryInterceptor? { 36 | if self.interceptorType.self is UnaryInterceptor.Type { 37 | return self.factory(config) as? UnaryInterceptor 38 | } 39 | return nil 40 | } 41 | 42 | func createStream(with config: ProtocolClientConfig) -> StreamInterceptor? { 43 | if self.interceptorType.self is StreamInterceptor.Type { 44 | return self.factory(config) as? StreamInterceptor 45 | } 46 | return nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Interceptors/UnaryInterceptor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Interceptor that can observe and/or mutate unary requests. 18 | public protocol UnaryInterceptor: Interceptor { 19 | /// Observe and/or mutate a typed request message to be sent to the server. 20 | /// 21 | /// Order of invocation during a request's lifecycle: 1 22 | /// 23 | /// - parameter request: The typed request and message to be sent. 24 | /// - parameter proceed: Closure which must be called to pass (potentially altered) request 25 | /// to the next interceptor. 26 | @Sendable 27 | func handleUnaryRequest( 28 | _ request: HTTPRequest, 29 | proceed: @escaping @Sendable (Result, ConnectError>) -> Void 30 | ) 31 | 32 | /// Observe and/or mutate a raw (serialized) request to be sent to the server. 33 | /// 34 | /// Order of invocation during a request's lifecycle: 2 (after `handleUnaryRequest()`) 35 | /// 36 | /// - parameter request: The raw (serialized) request to be sent. 37 | /// - parameter proceed: Closure which must be called to pass (potentially altered) request 38 | /// to the next interceptor. 39 | @Sendable 40 | func handleUnaryRawRequest( 41 | _ request: HTTPRequest, 42 | proceed: @escaping @Sendable (Result, ConnectError>) -> Void 43 | ) 44 | 45 | /// Observe and/or mutate a raw (serialized) response received from the server. 46 | /// 47 | /// Order of invocation during a request's lifecycle: 3 48 | /// 49 | /// - parameter response: The raw (serialized) response that was received. 50 | /// - parameter proceed: Closure which must be called to pass (potentially altered) response 51 | /// to the next interceptor. 52 | @Sendable 53 | func handleUnaryRawResponse( 54 | _ response: HTTPResponse, 55 | proceed: @escaping @Sendable (HTTPResponse) -> Void 56 | ) 57 | 58 | /// Observe and/or mutate a typed (deserialized) response received from the server. 59 | /// 60 | /// Order of invocation during a request's lifecycle: 4 (after `handleUnaryRawResponse()`) 61 | /// 62 | /// - parameter response: The typed (deserialized) response received from the server. 63 | /// - parameter proceed: Closure which must be called to pass (potentially altered) response 64 | /// to the next interceptor. 65 | @Sendable 66 | func handleUnaryResponse( 67 | _ response: ResponseMessage, 68 | proceed: @escaping @Sendable (ResponseMessage) -> Void 69 | ) 70 | } 71 | 72 | extension UnaryInterceptor { 73 | @Sendable 74 | public func handleUnaryRequest( 75 | _ request: HTTPRequest, 76 | proceed: @escaping @Sendable (Result, ConnectError>) -> Void 77 | ) { 78 | proceed(.success(request)) 79 | } 80 | 81 | @Sendable 82 | public func handleUnaryRawRequest( 83 | _ request: HTTPRequest, 84 | proceed: @escaping @Sendable (Result, ConnectError>) -> Void 85 | ) { 86 | proceed(.success(request)) 87 | } 88 | 89 | @Sendable 90 | public func handleUnaryRawResponse( 91 | _ response: HTTPResponse, 92 | proceed: @escaping @Sendable (HTTPResponse) -> Void 93 | ) { 94 | proceed(response) 95 | } 96 | 97 | @Sendable 98 | public func handleUnaryResponse( 99 | _ response: ResponseMessage, 100 | proceed: @escaping @Sendable (ResponseMessage) -> Void 101 | ) { 102 | proceed(response) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/MethodSpec.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Contains metadata for a specific RPC method. 16 | public struct MethodSpec: Equatable, Codable, Sendable { 17 | /// The name of the method (1:1 with the `.proto` file). E.g., `Foo`. 18 | public let name: String 19 | /// The fully qualified name of the method's service. E.g., `foo.v1.FooService`. 20 | public let service: String 21 | /// The type of method (unary, bidirectional stream, etc.). 22 | public let type: MethodType 23 | 24 | /// The path of the RPC, constructed using the package, service, and method name. 25 | /// E.g., `foo.v1.FooService/Foo`. 26 | public var path: String { 27 | return "\(self.service)/\(self.name)" 28 | } 29 | 30 | public enum MethodType: Equatable, Codable, Sendable { 31 | case unary 32 | case clientStream 33 | case serverStream 34 | case bidirectionalStream 35 | } 36 | 37 | public init(name: String, service: String, type: MethodType) { 38 | self.name = name 39 | self.service = service 40 | self.type = type 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/NetworkProtocol.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Protocols that are supported by the library. 16 | public enum NetworkProtocol: Sendable { 17 | /// The Connect protocol: 18 | /// https://connectrpc.com/docs/protocol 19 | case connect 20 | /// The gRPC-Web protocol: 21 | /// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md 22 | case grpcWeb 23 | /// A custom protocol that is implemented via an interceptor. 24 | case custom(name: String, protocolInterceptor: InterceptorFactory) 25 | } 26 | 27 | extension NetworkProtocol: CustomStringConvertible { 28 | public var description: String { 29 | switch self { 30 | case .connect: 31 | return "Connect" 32 | case .grpcWeb: 33 | return "gRPC-Web" 34 | case .custom(let name, _): 35 | return name 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/ProtobufMessage.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | public typealias ProtobufMessage = SwiftProtobuf.Message 18 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/ResponseMessage.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Typed unary response from an RPC. 18 | public struct ResponseMessage: Sendable { 19 | /// The status code of the response. 20 | public let code: Code 21 | /// Response headers specified by the server. 22 | public let headers: Headers 23 | /// The result of the RPC (either a message or an error). 24 | public let result: Result 25 | /// Trailers provided by the server. 26 | public let trailers: Trailers 27 | 28 | /// Convenience accessor for the `result`'s wrapped error. 29 | public var error: ConnectError? { 30 | switch self.result { 31 | case .success: 32 | return nil 33 | case .failure(let error): 34 | return error 35 | } 36 | } 37 | 38 | /// Convenience accessor for the `result`'s wrapped message. 39 | public var message: Output? { 40 | switch self.result { 41 | case .success(let message): 42 | return message 43 | case .failure: 44 | return nil 45 | } 46 | } 47 | 48 | public init( 49 | code: Code = .ok, headers: Headers = [:], 50 | result: Result, trailers: Trailers = [:] 51 | ) { 52 | self.code = code 53 | self.headers = headers 54 | self.result = result 55 | self.trailers = trailers 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/AsyncAwait/BidirectionalAsyncStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a bidirectional stream that can be interacted with using async/await. 18 | @available(iOS 13, *) 19 | public protocol BidirectionalAsyncStreamInterface: Sendable { 20 | /// The input (request) message type. 21 | associatedtype Input: ProtobufMessage 22 | 23 | /// The output (response) message type. 24 | associatedtype Output: ProtobufMessage 25 | 26 | /// Send a request to the server over the stream. 27 | /// 28 | /// - parameter input: The request message to send. 29 | /// 30 | /// - returns: An instance of this stream, for syntactic sugar. 31 | @discardableResult 32 | func send(_ input: Input) throws -> Self 33 | 34 | /// Obtain an await-able list of results from the stream using async/await. 35 | /// 36 | /// Example usage: `for await result in stream.results() {...}` 37 | /// 38 | /// - returns: An `AsyncStream` that contains all outputs/results from the stream. 39 | func results() -> AsyncStream> 40 | 41 | /// Close the stream. No calls to `send()` are valid after calling `close()`. 42 | func close() 43 | 44 | /// Cancel the stream and return a canceled code. 45 | /// No calls to `send()` are valid after calling `cancel()`. 46 | func cancel() 47 | } 48 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/AsyncAwait/ClientOnlyAsyncStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a client-only stream (a stream where the client streams data to the server and 18 | /// eventually receives a response) that can be interacted with using async/await. 19 | @available(iOS 13, *) 20 | public protocol ClientOnlyAsyncStreamInterface: Sendable { 21 | /// The input (request) message type. 22 | associatedtype Input: ProtobufMessage 23 | 24 | /// The output (response) message type. 25 | associatedtype Output: ProtobufMessage 26 | 27 | /// Send a request to the server over the stream. 28 | /// 29 | /// - parameter input: The request message to send. 30 | /// 31 | /// - returns: An instance of this stream, for syntactic sugar. 32 | @discardableResult 33 | func send(_ input: Input) throws -> Self 34 | 35 | /// Obtain an await-able list of results from the stream using async/await. 36 | /// 37 | /// Example usage: `for await result in stream.results() {...}` 38 | /// 39 | /// - returns: An `AsyncStream` that contains all outputs/results from the stream. 40 | func results() -> AsyncStream> 41 | 42 | /// Close the stream and await a response. 43 | /// No calls to `send()` are valid after calling `closeAndReceive()`. 44 | func closeAndReceive() 45 | 46 | /// Cancel the stream and return a canceled code. 47 | /// No calls to `send()` are valid after calling `cancel()`. 48 | func cancel() 49 | } 50 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/AsyncAwait/ServerOnlyAsyncStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a server-only stream (a stream where the server streams data to the client after 18 | /// receiving an initial request) that can be interacted with using async/await. 19 | @available(iOS 13, *) 20 | public protocol ServerOnlyAsyncStreamInterface: Sendable { 21 | /// The input (request) message type. 22 | associatedtype Input: ProtobufMessage 23 | 24 | /// The output (response) message type. 25 | associatedtype Output: ProtobufMessage 26 | 27 | /// Send a request to the server over the stream. 28 | /// 29 | /// Should be called exactly one time when starting the stream. 30 | /// 31 | /// - parameter input: The request message to send. 32 | func send(_ input: Input) throws 33 | 34 | /// Obtain an await-able list of results from the stream using async/await. 35 | /// 36 | /// Example usage: `for await result in stream.results() {...}` 37 | /// 38 | /// - returns: An `AsyncStream` that contains all outputs/results from the stream. 39 | func results() -> AsyncStream> 40 | 41 | /// Cancel the stream and return a canceled code. 42 | /// No calls to `send()` are valid after calling `cancel()`. 43 | func cancel() 44 | } 45 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/Callbacks/BidirectionalStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a bidirectional stream that can send request messages and initiate closes. 18 | public protocol BidirectionalStreamInterface { 19 | /// The input (request) message type. 20 | associatedtype Input: ProtobufMessage 21 | 22 | /// Send a request to the server over the stream. 23 | /// 24 | /// - parameter input: The request message to send. 25 | /// 26 | /// - returns: An instance of this stream, for syntactic sugar. 27 | @discardableResult 28 | func send(_ input: Input) -> Self 29 | 30 | /// Close the stream. No calls to `send()` are valid after calling `close()`. 31 | func close() 32 | 33 | /// Cancel the stream and return a canceled code. 34 | /// No calls to `send()` are valid after calling `cancel()`. 35 | func cancel() 36 | } 37 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/Callbacks/ClientOnlyStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a client-only stream (a stream where the client streams data to the server and 18 | /// eventually receives a response) that can send request messages and initiate closes. 19 | public protocol ClientOnlyStreamInterface { 20 | /// The input (request) message type. 21 | associatedtype Input: ProtobufMessage 22 | 23 | /// Send a request to the server over the stream. 24 | /// 25 | /// - parameter input: The request message to send. 26 | /// 27 | /// - returns: An instance of this stream, for syntactic sugar. 28 | @discardableResult 29 | func send(_ input: Input) throws -> Self 30 | 31 | /// Close the stream and await a response. 32 | /// No calls to `send()` are valid after calling `closeAndReceive()`. 33 | func closeAndReceive() 34 | 35 | /// Cancel the stream and return a canceled code. 36 | /// No calls to `send()` are valid after calling `cancel()`. 37 | func cancel() 38 | } 39 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/Callbacks/RequestCallbacks.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Set of closures that are used for wiring outbound request data through to HTTP clients. 16 | public final class RequestCallbacks: Sendable { 17 | /// Closure to cancel the request from the client. 18 | public let cancel: @Sendable () -> Void 19 | /// Closure to send data through to the server. 20 | public let sendData: @Sendable (T) -> Void 21 | /// Closure to initiate a close for a stream. 22 | public let sendClose: @Sendable () -> Void 23 | 24 | public init( 25 | cancel: @escaping @Sendable () -> Void, 26 | sendData: @escaping @Sendable (T) -> Void, 27 | sendClose: @escaping @Sendable () -> Void 28 | ) { 29 | self.cancel = cancel 30 | self.sendData = sendData 31 | self.sendClose = sendClose 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/Callbacks/ResponseCallbacks.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Set of closures that are used for wiring inbound response data through from HTTP clients. 18 | public final class ResponseCallbacks: Sendable { 19 | /// Closure to call when response headers are available. 20 | public let receiveResponseHeaders: @Sendable (Headers) -> Void 21 | /// Closure to call when response data is available. 22 | public let receiveResponseData: @Sendable (Data) -> Void 23 | /// Closure to call when response metrics are available. 24 | public let receiveResponseMetrics: @Sendable (HTTPMetrics) -> Void 25 | /// Closure to call when the stream is closed. 26 | /// Includes the status code, trailers, and potentially an error. 27 | public let receiveClose: @Sendable (Code, Trailers, Swift.Error?) -> Void 28 | 29 | public init( 30 | receiveResponseHeaders: @escaping @Sendable (Headers) -> Void, 31 | receiveResponseData: @escaping @Sendable (Data) -> Void, 32 | receiveResponseMetrics: @escaping @Sendable (HTTPMetrics) -> Void, 33 | receiveClose: @escaping @Sendable (Code, Trailers, Swift.Error?) -> Void 34 | ) { 35 | self.receiveResponseHeaders = receiveResponseHeaders 36 | self.receiveResponseData = receiveResponseData 37 | self.receiveResponseMetrics = receiveResponseMetrics 38 | self.receiveClose = receiveClose 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/Callbacks/ServerOnlyStreamInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobuf 16 | 17 | /// Represents a server-only stream (a stream where the server streams data to the client after 18 | /// receiving an initial request) that can send request messages. 19 | public protocol ServerOnlyStreamInterface { 20 | /// The input (request) message type. 21 | associatedtype Input: ProtobufMessage 22 | 23 | /// Send a request to the server over the stream. 24 | /// 25 | /// Should be called exactly one time when starting the stream. 26 | /// 27 | /// - parameter input: The request message to send. 28 | func send(_ input: Input) 29 | 30 | /// Cancel the stream and return a canceled code. 31 | /// No calls to `send()` are valid after calling `cancel()`. 32 | func cancel() 33 | } 34 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Streaming/StreamResult.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Enumeration of result states that can be received over streams. 16 | /// 17 | /// A typical stream receives `headers > message > message > message ... > complete`. 18 | @frozen 19 | public enum StreamResult: Sendable { 20 | /// Stream is complete. Provides the end status code and optionally an error and trailers. 21 | case complete(code: Code, error: Swift.Error?, trailers: Trailers?) 22 | /// Headers have been received over the stream. 23 | case headers(Headers) 24 | /// A response message has been received over the stream. 25 | case message(Output) 26 | 27 | public var messageValue: Output? { 28 | switch self { 29 | case .headers, .complete: 30 | return nil 31 | case .message(let output): 32 | return output 33 | } 34 | } 35 | } 36 | 37 | extension StreamResult: Equatable where Output: Equatable { 38 | public static func == (lhs: StreamResult, rhs: StreamResult) -> Bool { 39 | switch (lhs, rhs) { 40 | case ( 41 | .complete(let code1, let error1, let trailers1), 42 | .complete(let code2, let error2, let trailers2) 43 | ): 44 | return code1 == code2 45 | && trailers1 == trailers2 46 | && (error1 != nil) == (error2 != nil) 47 | 48 | case (.headers(let headers1), .headers(let headers2)): 49 | return headers1 == headers2 50 | 51 | case (.message(let message1), .message(let message2)): 52 | return message1 == message2 53 | 54 | default: 55 | return false 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Libraries/Connect/Public/Interfaces/Trailers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Request/response trailers. 16 | /// All keys are expected to be lowercased. 17 | /// Comma-separated values are split into individual items in the array. For example: 18 | /// On the wire: `accept-encoding: gzip,brotli` or `accept-encoding: gzip, brotli` 19 | /// Yields: `["accept-encoding": ["gzip", "brotli"]]` 20 | public typealias Trailers = [String: [String]] 21 | -------------------------------------------------------------------------------- /Libraries/Connect/README.md: -------------------------------------------------------------------------------- 1 | ## Connect 2 | 3 | This module contains support for the Connect and gRPC-Web protocols. 4 | For gRPC support, you must also include the `ConnectNIO` module. 5 | 6 | For additional details and tutorials, see the 7 | [Connect-Swift documentation](https://connectrpc.com/docs/swift/getting-started/). 8 | -------------------------------------------------------------------------------- /Libraries/Connect/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/apple/swift:v1.28.2 4 | opt: Visibility=Internal 5 | out: ./Internal/GeneratedSources 6 | -------------------------------------------------------------------------------- /Libraries/Connect/proto/README.md: -------------------------------------------------------------------------------- 1 | This directory contains `.proto` files whose outputs are compiled into the 2 | Connect library. 3 | -------------------------------------------------------------------------------- /Libraries/Connect/proto/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | lint: 3 | use: 4 | - DEFAULT 5 | ignore: 6 | - grpc/status/v1/status.proto 7 | breaking: 8 | use: 9 | - WIRE_JSON 10 | -------------------------------------------------------------------------------- /Libraries/Connect/proto/grpc/status/v1/status.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | // This package is for internal use by Connect, and provides no backward 18 | // compatibility guarantees whatsoever. 19 | package grpc.status.v1; 20 | 21 | import "google/protobuf/any.proto"; 22 | 23 | // See https://cloud.google.com/apis/design/errors. 24 | // 25 | // This struct must remain binary-compatible with 26 | // https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto. 27 | message Status { 28 | int32 code = 1; // a google.rpc.Code 29 | string message = 2; // developer-facing, English (localize in details or client-side) 30 | repeated google.protobuf.Any details = 3; 31 | } 32 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockBidirectionalAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Connect 17 | import SwiftProtobuf 18 | 19 | /// Mock implementation of `BidirectionalAsyncStreamInterface` which can be used for testing. 20 | /// 21 | /// This type can be used by setting `on*` closures and observing their calls, 22 | /// by validating its instance variables such as `inputs` at the end of invocation, 23 | /// or by subclassing the type and overriding functions such as `send()`. 24 | /// 25 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)` or by 26 | /// subclassing and overriding `results()`. 27 | @available(iOS 13, *) 28 | open class MockBidirectionalAsyncStream< 29 | Input: ProtobufMessage, 30 | Output: ProtobufMessage 31 | >: BidirectionalAsyncStreamInterface, @unchecked Sendable { 32 | /// Used to store cancellables from the stream. 33 | private var cancellables = [AnyCancellable]() 34 | 35 | /// Closure that is called when `close()` is invoked. 36 | public var onClose: (() -> Void)? 37 | /// Closure that is called when `send()` is invoked. 38 | public var onSend: ((Input) -> Void)? 39 | /// The list of outputs to return to calls to the `results()` function 40 | /// once one input has been sent. 41 | public var outputs: [StreamResult] 42 | 43 | /// All inputs that have been sent through the stream. 44 | @Published public private(set) var inputs = [Input]() 45 | /// True if `close()` has been called. 46 | @Published public private(set) var isClosed = false 47 | 48 | /// Designated initializer. 49 | /// 50 | /// - parameter outputs: The list of outputs to return to calls to the `results()` function once 51 | /// one input has been sent. 52 | public init(outputs: [StreamResult] = []) { 53 | self.outputs = outputs 54 | } 55 | 56 | @discardableResult 57 | open func send(_ input: Input) throws -> Self { 58 | self.inputs.append(input) 59 | self.onSend?(input) 60 | return self 61 | } 62 | 63 | open func results() -> AsyncStream> { 64 | // Wait until a request is sent over the stream to return the results. 65 | return AsyncStream { continuation in 66 | self.$inputs 67 | .first { !$0.isEmpty } 68 | .sink { _ in 69 | for output in self.outputs { 70 | continuation.yield(output) 71 | } 72 | continuation.finish() 73 | } 74 | .store(in: &self.cancellables) 75 | } 76 | } 77 | 78 | open func close() { 79 | self.isClosed = true 80 | self.onClose?() 81 | } 82 | 83 | open func cancel() {} 84 | } 85 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockBidirectionalStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Connect 17 | import SwiftProtobuf 18 | 19 | /// Mock implementation of `BidirectionalStreamInterface` which can be used for testing. 20 | /// 21 | /// This type can be used by setting `on*` closures and observing their calls, 22 | /// by validating its instance variables such as `inputs` at the end of invocation, 23 | /// or by subclassing the type and overriding functions such as `send()`. 24 | /// 25 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)`. 26 | @available(iOS 13, *) 27 | open class MockBidirectionalStream< 28 | Input: ProtobufMessage, 29 | Output: ProtobufMessage 30 | >: BidirectionalStreamInterface, @unchecked Sendable { 31 | /// Closure that is called when `close()` is invoked. 32 | public var onClose: (() -> Void)? 33 | /// Closure that is called when `send()` is invoked. 34 | public var onSend: ((Input) -> Void)? 35 | /// The list of outputs to return to the client once one input has been sent. 36 | public var outputs: [StreamResult] 37 | 38 | /// All inputs that have been sent through the stream. 39 | @Published public private(set) var inputs = [Input]() 40 | /// True if `close()` has been called. 41 | @Published public private(set) var isClosed = false 42 | 43 | /// Designated initializer. 44 | /// 45 | /// - parameter outputs: The list of outputs to return to the client once one input has been 46 | /// sent. 47 | public init(outputs: [StreamResult] = []) { 48 | self.outputs = outputs 49 | } 50 | 51 | @discardableResult 52 | open func send(_ input: Input) -> Self { 53 | self.inputs.append(input) 54 | self.onSend?(input) 55 | return self 56 | } 57 | 58 | open func close() { 59 | self.isClosed = true 60 | self.onClose?() 61 | } 62 | 63 | open func cancel() {} 64 | } 65 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockClientOnlyAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import SwiftProtobuf 17 | 18 | /// Mock implementation of `ClientOnlyAsyncStreamInterface` which can be used for testing. 19 | /// 20 | /// This type can be used by setting `on*` closures and observing their calls, 21 | /// by validating its instance variables such as `inputs` at the end of invocation, 22 | /// or by subclassing the type and overriding functions such as `send()`. 23 | /// 24 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)` or by 25 | /// subclassing and overriding `results()`. 26 | @available(iOS 13, *) 27 | open class MockClientOnlyAsyncStream< 28 | Input: ProtobufMessage, 29 | Output: ProtobufMessage 30 | >: MockBidirectionalAsyncStream, ClientOnlyAsyncStreamInterface, @unchecked Sendable 31 | { 32 | open func closeAndReceive() { 33 | self.close() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockClientOnlyStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import SwiftProtobuf 17 | 18 | /// Mock implementation of `ClientOnlyStreamInterface` which can be used for testing. 19 | /// 20 | /// This type can be used by setting `on*` closures and observing their calls, 21 | /// by validating its instance variables such as `inputs` at the end of invocation, 22 | /// or by subclassing the type and overriding functions such as `send()`. 23 | /// 24 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)`. 25 | @available(iOS 13, *) 26 | open class MockClientOnlyStream< 27 | Input: ProtobufMessage, 28 | Output: ProtobufMessage 29 | >: MockBidirectionalStream, ClientOnlyStreamInterface, @unchecked Sendable { 30 | open func closeAndReceive() { 31 | self.close() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockServerOnlyAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Connect 17 | import SwiftProtobuf 18 | 19 | /// Mock implementation of `ServerOnlyAsyncStreamInterface` which can be used for testing. 20 | /// 21 | /// This type can be used by setting `on*` closures and observing their calls, 22 | /// by validating its instance variables such as `inputs` at the end of invocation, 23 | /// or by subclassing the type and overriding functions such as `send()`. 24 | /// 25 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)` or by 26 | /// subclassing and overriding `results()`. 27 | @available(iOS 13, *) 28 | open class MockServerOnlyAsyncStream< 29 | Input: ProtobufMessage, 30 | Output: ProtobufMessage 31 | >: ServerOnlyAsyncStreamInterface, @unchecked Sendable { 32 | /// Used to store cancellables from the stream. 33 | private var cancellables = [AnyCancellable]() 34 | 35 | /// Closure that is called when `send()` is invoked. 36 | public var onSend: ((Input) -> Void)? 37 | /// The list of outputs to return to calls to the `results()` function 38 | /// once one input has been sent. 39 | public var outputs: [StreamResult] 40 | 41 | /// All inputs that have been sent through the stream. 42 | @Published public private(set) var inputs = [Input]() 43 | 44 | /// Designated initializer. 45 | /// 46 | /// - parameter outputs: The list of outputs to return to calls to the `results()` function once 47 | /// one input has been sent. 48 | public init(outputs: [StreamResult] = []) { 49 | self.outputs = outputs 50 | } 51 | 52 | open func send(_ input: Input) throws { 53 | self.inputs.append(input) 54 | self.onSend?(input) 55 | } 56 | 57 | open func results() -> AsyncStream> { 58 | // Wait until a request is sent over the stream to return the results. 59 | return AsyncStream { continuation in 60 | self.$inputs 61 | .first { !$0.isEmpty } 62 | .sink { _ in 63 | for output in self.outputs { 64 | continuation.yield(output) 65 | } 66 | continuation.finish() 67 | } 68 | .store(in: &self.cancellables) 69 | } 70 | } 71 | 72 | open func cancel() {} 73 | } 74 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/MockServerOnlyStream.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Connect 17 | import SwiftProtobuf 18 | 19 | /// Mock implementation of `ServerOnlyStreamInterface` which can be used for testing. 20 | /// 21 | /// This type can be used by setting `on*` closures and observing their calls, 22 | /// by validating its instance variables such as `inputs` at the end of invocation, 23 | /// or by subclassing the type and overriding functions such as `send()`. 24 | /// 25 | /// To return data over the stream, outputs can be specified using `init(outputs: ...)`. 26 | @available(iOS 13, *) 27 | open class MockServerOnlyStream< 28 | Input: ProtobufMessage, 29 | Output: ProtobufMessage 30 | >: ServerOnlyStreamInterface, @unchecked Sendable { 31 | /// Closure that is called when `send()` is invoked. 32 | public var onSend: ((Input) -> Void)? 33 | /// The list of outputs to return to the client once one input has been sent. 34 | public var outputs: [StreamResult] 35 | 36 | /// All inputs that have been sent through the stream. 37 | @Published public private(set) var inputs = [Input]() 38 | 39 | /// Designated initializer. 40 | /// 41 | /// - parameter outputs: The list of outputs to return to the client once one input has been 42 | /// sent. 43 | public init(outputs: [StreamResult] = []) { 44 | self.outputs = outputs 45 | } 46 | 47 | open func send(_ input: Input) { 48 | self.inputs.append(input) 49 | self.onSend?(input) 50 | } 51 | 52 | open func cancel() {} 53 | } 54 | -------------------------------------------------------------------------------- /Libraries/ConnectMocks/README.md: -------------------------------------------------------------------------------- 1 | ## ConnectMocks 2 | 3 | This module contains types that are designed to make mocking/testing generated 4 | Connect clients and methods easier. 5 | 6 | The typical workflow for consuming these mocks is: 7 | 8 | 1. Invoke the `connect-swift-mocks` plugin. 9 | 2. The plugin will output corresponding `*.mock.swift` files which import `ConnectMocks` to provide default no-op implementations for each generated `service` and `rpc`. 10 | 3. Import both the `ConnectMocks` module and the generated files into your test suite. 11 | 4. Assuming your code uses the `*Interface` types (rather than their concrete implementation types), you can inject the corresponding `*Mock` types (instead of the generated `*Client` types as you would in production) and easily replace production RPC calls with mocked out test data. 12 | -------------------------------------------------------------------------------- /Libraries/ConnectNIO/Internal/Extensions/ConnectError+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | 17 | extension ConnectError { 18 | static func deadlineExceeded() -> Self { 19 | return .init(code: .deadlineExceeded, message: "timed out") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Libraries/ConnectNIO/Internal/Extensions/HTTPRequestHead+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import Foundation 17 | import NIOHTTP1 18 | 19 | extension HTTPRequestHead { 20 | static func fromConnect( 21 | _ request: Connect.HTTPRequest, nioHeaders: NIOHTTP1.HTTPHeaders 22 | ) -> Self { 23 | switch request.method { 24 | case .get: 25 | return HTTPRequestHead( 26 | version: .http1_1, 27 | method: .GET, 28 | uri: "\(request.url.path)?\(request.url.query ?? "")", 29 | headers: nioHeaders 30 | ) 31 | case .post: 32 | return HTTPRequestHead( 33 | version: .http1_1, 34 | method: .POST, 35 | uri: request.url.path, 36 | headers: nioHeaders 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Libraries/ConnectNIO/Internal/Extensions/Headers+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import Foundation 17 | import NIOHTTP1 18 | 19 | // MARK: - NIO type extensions 20 | 21 | extension NIOHTTP1.HTTPHeaders { 22 | mutating func addNIOHeadersFromConnect(_ headers: Connect.Headers) { 23 | for (name, value) in headers { 24 | self.add(name: name, value: value.joined(separator: ",")) 25 | } 26 | } 27 | } 28 | 29 | // MARK: - Connect type extensions 30 | 31 | extension Headers { 32 | static func fromNIOHeaders(_ nioHeaders: NIOHTTP1.HTTPHeaders) -> Self { 33 | return nioHeaders.reduce(into: [:]) { headers, current in 34 | let headerName = current.name.lowercased() 35 | for value in current.value.components(separatedBy: ",") { 36 | headers[headerName, default: []].append(value.trimmingCharacters(in: .whitespaces)) 37 | } 38 | } 39 | } 40 | } 41 | 42 | extension Code { 43 | static func fromNIOStatus(_ nioStatus: NIOHTTP1.HTTPResponseStatus) -> Self { 44 | return .fromHTTPStatus(Int(nioStatus.code)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Libraries/ConnectNIO/Public/NetworkProtocol+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | 17 | extension NetworkProtocol { 18 | /// The gRPC protocol: 19 | /// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md 20 | /// 21 | /// IMPORTANT: This protocol must be used in conjunction with an HTTP client that supports 22 | /// trailers, such as the `NIOHTTPClient` included in this library. 23 | public static var grpc: Self { 24 | return .custom( 25 | name: "gRPC", 26 | protocolInterceptor: InterceptorFactory { GRPCInterceptor(config: $0) } 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Libraries/ConnectNIO/README.md: -------------------------------------------------------------------------------- 1 | ## ConnectNIO 2 | 3 | This module provides an `NIOHTTPClient` which conforms to the Connect 4 | library's `HTTPClientInterface` protocol and is backed by the 5 | [SwiftNIO](https://github.com/apple/swift-nio) networking stack. 6 | 7 | Additionally, since SwiftNIO supports trailers, this module provides support 8 | for using the gRPC protocol alongside the Connect and gRPC-Web protocols 9 | provided by the main Connect library. 10 | 11 | This library is unavailable through CocoaPods since 12 | [SwiftNIO does not support CocoaPods](https://github.com/apple/swift-nio/issues/2393), 13 | and it can only be consumed using Swift Package Manager. 14 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Maintainers 2 | =========== 3 | 4 | ## Current 5 | * [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build) 6 | * [Michael Rebello](https://github.com/rebello95), [Airbnb](https://airbnb.com) 7 | * [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build) 8 | * [Eddie Seay](https://github.com/eseay), [Chick-fil-A](https://www.chick-fil-a.com/) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See https://tech.davis-hansson.com/p/make/ 2 | SHELL := bash 3 | .DELETE_ON_ERROR: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | .DEFAULT_GOAL := all 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | MAKEFLAGS += --no-print-directory 9 | BIN := .tmp/bin 10 | LICENSE_HEADER_YEAR_RANGE := 2022-2025 11 | CONFORMANCE_PROTO_REF := 9cb891ae36142a8a19c76f1e8a615b6d41fb726b 12 | CONFORMANCE_RUNNER_TAG := v1.0.4 13 | EXAMPLES_PROTO_REF := e74547031f662f81a62f5e95ebaa9f7037e0c41b 14 | LICENSE_HEADER_VERSION := v1.35.1 15 | LICENSE_IGNORE := -e Package.swift \ 16 | -e $(BIN)\/ \ 17 | -e Examples/ElizaSharedSources/GeneratedSources\/ \ 18 | -e Libraries/Connect/Internal/GeneratedSources\/ \ 19 | -e Tests/ConformanceClient/GeneratedSources\/ \ 20 | -e Tests/UnitTests/ConnectLibraryTests/GeneratedSources\/ 21 | 22 | .PHONY: buildpackage 23 | buildpackage: ## Build all targets in the Swift package 24 | swift build 25 | 26 | .PHONY: buildplugins 27 | buildplugins: ## Build all plugin binaries 28 | mkdir -p $(BIN) 29 | swift build -c release --product protoc-gen-connect-swift 30 | mv ./.build/release/protoc-gen-connect-swift $(BIN) 31 | swift build -c release --product protoc-gen-connect-swift-mocks 32 | mv ./.build/release/protoc-gen-connect-swift-mocks $(BIN) 33 | @echo "Success! Plugins are available in $(BIN)" 34 | 35 | .PHONY: clean 36 | clean: cleangenerated ## Delete all plugins and generated outputs 37 | rm -rf $(BIN) 38 | 39 | .PHONY: cleangenerated 40 | cleangenerated: ## Delete all generated outputs 41 | rm -rf ./Examples/ElizaSharedSources/GeneratedSources/* 42 | rm -rf ./Libraries/Connect/Internal/GeneratedSources/* 43 | rm -rf ./Tests/ConformanceClient/GeneratedSources/* 44 | rm -rf ./Tests/UnitTests/ConnectLibraryTests/GeneratedSources/* 45 | 46 | .PHONY: generate 47 | generate: cleangenerated ## Regenerate outputs for all .proto files 48 | cd Examples; buf generate https://github.com/connectrpc/examples-go.git#tag=$(EXAMPLES_PROTO_REF),subdir=proto 49 | cd Libraries/Connect; buf generate 50 | cd Tests/ConformanceClient; buf generate https://github.com/connectrpc/conformance.git#tag=$(CONFORMANCE_PROTO_REF),subdir=proto 51 | cd Tests/UnitTests/ConnectLibraryTests; buf generate https://github.com/connectrpc/conformance.git#tag=$(CONFORMANCE_PROTO_REF),subdir=proto 52 | 53 | .PHONY: installconformancerunner 54 | installconformancerunner: ## Install the Connect conformance test runner 55 | mkdir -p $(BIN) 56 | curl -L "https://github.com/connectrpc/conformance/releases/download/$(CONFORMANCE_RUNNER_TAG)/connectconformance-$(CONFORMANCE_RUNNER_TAG)-$(shell uname -s)-$(shell uname -m).tar.gz" > $(BIN)/connectconformance.tar.gz 57 | tar -xvzf $(BIN)/connectconformance.tar.gz -C $(BIN) 58 | 59 | .PHONY: help 60 | help: ## Describe useful make targets 61 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 62 | 63 | .PHONY: licenseheaders 64 | licenseheaders: $(BIN)/license-headers ## Add/reformat license headers in source files 65 | comm -23 \ 66 | <(git ls-files --cached --modified --others --no-empty-directory --exclude-standard | sort -u | grep -v $(LICENSE_IGNORE) ) \ 67 | <(git ls-files --deleted | sort -u) | \ 68 | xargs $(BIN)/license-header \ 69 | --license-type "apache" \ 70 | --copyright-holder "The Connect Authors" \ 71 | --year-range "$(LICENSE_HEADER_YEAR_RANGE)" 72 | 73 | $(BIN)/license-headers: Makefile 74 | mkdir -p $(@D) 75 | GOBIN=$(abspath $(BIN)) go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@$(LICENSE_HEADER_VERSION) 76 | 77 | .PHONY: testconformance 78 | testconformance: ## Run all conformance tests 79 | swift build -c release --product ConnectConformanceClient 80 | mv ./.build/release/ConnectConformanceClient $(BIN) 81 | PATH="$(abspath $(BIN)):$(PATH)" connectconformance --trace --conf ./Tests/ConformanceClient/InvocationConfigs/urlsession.yaml --mode client $(BIN)/ConnectConformanceClient httpclient=urlsession 82 | PATH="$(abspath $(BIN)):$(PATH)" connectconformance --trace --conf ./Tests/ConformanceClient/InvocationConfigs/nio.yaml --mode client $(BIN)/ConnectConformanceClient httpclient=nio 83 | 84 | .PHONY: testunit 85 | testunit: ## Run all unit tests 86 | go install connectrpc.com/conformance/cmd/referenceserver@$(CONFORMANCE_RUNNER_TAG) 87 | echo "{\"protocol\": \"PROTOCOL_CONNECT\", \"httpVersion\": \"HTTP_VERSION_1\"}" | go run connectrpc.com/conformance/cmd/referenceserver@$(CONFORMANCE_RUNNER_TAG) -port 52107 -json & 88 | swift test 89 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-atomics", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-atomics.git", 7 | "state" : { 8 | "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", 9 | "version" : "1.1.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 18 | "version" : "1.1.4" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-nio", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-nio", 25 | "state" : { 26 | "revision" : "c51907a839e63ebf0ba2076bba73dd96436bd1b9", 27 | "version" : "2.81.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-nio-http2", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-nio-http2.git", 34 | "state" : { 35 | "revision" : "170f4ca06b6a9c57b811293cebcb96e81b661310", 36 | "version" : "1.35.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-nio-ssl", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-nio-ssl.git", 43 | "state" : { 44 | "revision" : "0cc3528ff48129d64ab9cab0b1cd621634edfc6b", 45 | "version" : "2.29.3" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-protobuf", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-protobuf.git", 52 | "state" : { 53 | "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", 54 | "version" : "1.28.2" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-system", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-system.git", 61 | "state" : { 62 | "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", 63 | "version" : "1.4.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Plugins/ConnectPluginUtilities/FilePathComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Used to split a file path into its specific components. 16 | public struct FilePathComponents { 17 | /// Example: "/foo/bar" from a "/foo/bar/baz.proto" file path. 18 | let directory: String 19 | /// Example: "baz" from a "/foo/bar/baz.proto" file path. 20 | let base: String 21 | /// Example: ".proto" from a "/foo/bar/baz.proto" file path. 22 | let suffix: String 23 | 24 | /// Parse the path components from a file path. 25 | /// 26 | /// - parameter path: For example, "/foo/bar/baz.proto" or "foo/bar/baz.proto". 27 | public init(path: String) { 28 | let allComponents = path.components(separatedBy: "/") 29 | let fileNameComponents = allComponents.last!.split(separator: ".") 30 | self.directory = allComponents.dropLast().joined(separator: "/") 31 | self.base = fileNameComponents.dropLast().joined(separator: ".") 32 | self.suffix = fileNameComponents.count > 1 ? "." + fileNameComponents.last! : "" 33 | } 34 | 35 | public func outputFilePath( 36 | withExtension pathExtension: String, 37 | using option: GeneratorOptions.FileNaming 38 | ) -> String { 39 | if self.directory.isEmpty { 40 | return "\(self.base)\(pathExtension)" 41 | } 42 | 43 | switch option { 44 | case .dropPath: 45 | return "\(self.base)\(pathExtension)" 46 | 47 | case .fullPath: 48 | return "\(self.directory)/\(self.base)\(pathExtension)" 49 | 50 | case .pathToUnderscores: 51 | let underscoredDirectory = self.directory.replacingOccurrences(of: "/", with: "_") 52 | if underscoredDirectory.hasPrefix("_") { 53 | return "\(underscoredDirectory.dropFirst())_\(self.base)\(pathExtension)" 54 | } else { 55 | return "\(underscoredDirectory)_\(self.base)\(pathExtension)" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Plugins/ConnectPluginUtilities/MethodDescriptor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobufPluginLibrary 16 | 17 | extension MethodDescriptor { 18 | public var methodPath: String { 19 | return "/\(self.service.servicePath)/\(self.name)" 20 | } 21 | 22 | public func name(using options: GeneratorOptions) -> String { 23 | return options.keepMethodCasing 24 | ? self.name 25 | : NamingUtils.toLowerCamelCase(self.name) 26 | } 27 | 28 | public func callbackSignature( 29 | using namer: SwiftProtobufNamer, includeDefaults: Bool, options: GeneratorOptions 30 | ) -> String { 31 | let methodName = self.name(using: options) 32 | let inputName = namer.fullName(message: self.inputType) 33 | let outputName = namer.fullName(message: self.outputType) 34 | 35 | // Note that the method name is escaped to avoid using Swift keywords. 36 | if self.clientStreaming && self.serverStreaming { 37 | return """ 38 | func `\(methodName)`\ 39 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : ""), \ 40 | onResult: @escaping @Sendable (Connect.StreamResult<\(outputName)>) -> Void) \ 41 | -> any Connect.BidirectionalStreamInterface<\(inputName)> 42 | """ 43 | } else if self.serverStreaming { 44 | return """ 45 | func `\(methodName)`\ 46 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : ""), \ 47 | onResult: @escaping @Sendable (Connect.StreamResult<\(outputName)>) -> Void) \ 48 | -> any Connect.ServerOnlyStreamInterface<\(inputName)> 49 | """ 50 | } else if self.clientStreaming { 51 | return """ 52 | func `\(methodName)`\ 53 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : ""), \ 54 | onResult: @escaping @Sendable (Connect.StreamResult<\(outputName)>) -> Void) \ 55 | -> any Connect.ClientOnlyStreamInterface<\(inputName)> 56 | """ 57 | } else { 58 | return """ 59 | func `\(methodName)`\ 60 | (request: \(inputName), headers: Connect.Headers\(includeDefaults ? " = [:]" : ""), \ 61 | completion: @escaping @Sendable (ResponseMessage<\(outputName)>) -> Void) \ 62 | -> Connect.Cancelable 63 | """ 64 | } 65 | } 66 | 67 | public func asyncAwaitSignature( 68 | using namer: SwiftProtobufNamer, includeDefaults: Bool, options: GeneratorOptions 69 | ) -> String { 70 | let methodName = self.name(using: options) 71 | let inputName = namer.fullName(message: self.inputType) 72 | let outputName = namer.fullName(message: self.outputType) 73 | 74 | // Note that the method name is escaped to avoid using Swift keywords. 75 | if self.clientStreaming && self.serverStreaming { 76 | return """ 77 | func `\(methodName)`\ 78 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : "")) \ 79 | -> any Connect.BidirectionalAsyncStreamInterface<\(inputName), \(outputName)> 80 | """ 81 | } else if self.serverStreaming { 82 | return """ 83 | func `\(methodName)`\ 84 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : "")) \ 85 | -> any Connect.ServerOnlyAsyncStreamInterface<\(inputName), \(outputName)> 86 | """ 87 | } else if self.clientStreaming { 88 | return """ 89 | func `\(methodName)`\ 90 | (headers: Connect.Headers\(includeDefaults ? " = [:]" : "")) \ 91 | -> any Connect.ClientOnlyAsyncStreamInterface<\(inputName), \(outputName)> 92 | """ 93 | } else { 94 | return """ 95 | func `\(methodName)`\ 96 | (request: \(inputName), headers: Connect.Headers\(includeDefaults ? " = [:]" : "")) \ 97 | async -> ResponseMessage<\(outputName)> 98 | """ 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Plugins/ConnectPluginUtilities/ServiceDescriptor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftProtobufPluginLibrary 16 | 17 | extension ServiceDescriptor { 18 | public var servicePath: String { 19 | if self.file.package.isEmpty { 20 | return self.name 21 | } else { 22 | return "\(self.file.package).\(self.name)" 23 | } 24 | } 25 | 26 | public func implementationName(using namer: SwiftProtobufNamer) -> String { 27 | let upperCamelName = NamingUtils.toUpperCamelCase(self.name) + "Client" 28 | if self.file.package.isEmpty { 29 | return upperCamelName 30 | } else { 31 | return namer.typePrefix(forFile: self.file) + upperCamelName 32 | } 33 | } 34 | 35 | public func protocolName(using namer: SwiftProtobufNamer) -> String { 36 | return self.implementationName(using: namer) + "Interface" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Security Policy 2 | =============== 3 | 4 | This project follows the [Connect security policy and reporting 5 | process](https://connectrpc.com/docs/governance/security). 6 | -------------------------------------------------------------------------------- /Tests/ConformanceClient/InvocationConfigs/nio.yaml: -------------------------------------------------------------------------------- 1 | features: 2 | codecs: 3 | - CODEC_PROTO 4 | - CODEC_JSON 5 | compressions: 6 | - COMPRESSION_IDENTITY 7 | - COMPRESSION_GZIP 8 | protocols: 9 | - PROTOCOL_CONNECT 10 | - PROTOCOL_GRPC_WEB 11 | - PROTOCOL_GRPC 12 | supports_connect_get: true 13 | supports_half_duplex_bidi_over_http1: true 14 | supports_message_receive_limit: false 15 | supports_h2c: true 16 | supports_tls: false 17 | supports_tls_client_certs: false 18 | supports_trailers: true 19 | versions: 20 | - HTTP_VERSION_2 21 | -------------------------------------------------------------------------------- /Tests/ConformanceClient/InvocationConfigs/urlsession.yaml: -------------------------------------------------------------------------------- 1 | features: 2 | codecs: 3 | - CODEC_PROTO 4 | - CODEC_JSON 5 | compressions: 6 | - COMPRESSION_IDENTITY 7 | - COMPRESSION_GZIP 8 | protocols: 9 | - PROTOCOL_CONNECT 10 | - PROTOCOL_GRPC_WEB 11 | supports_connect_get: true 12 | supports_half_duplex_bidi_over_http1: true 13 | supports_message_receive_limit: false 14 | supports_h2c: false 15 | supports_tls: false 16 | supports_tls_client_certs: false 17 | supports_trailers: false 18 | versions: 19 | - HTTP_VERSION_1 20 | -------------------------------------------------------------------------------- /Tests/ConformanceClient/README.md: -------------------------------------------------------------------------------- 1 | # ConformanceClient 2 | 3 | This directory contains an executable which can be invoked by the 4 | [Connect conformance runner](https://github.com/connectrpc/conformance) 5 | which performs a matrix of hundreds of runtime tests against a local 6 | server in order to validate library behaviors with various permutations of 7 | protocols, codecs, etc. 8 | 9 | Refer to the [contributing guide](../../.github/CONTRIBUTING.md#running-tests) 10 | for details on how to run these tests with the runner. 11 | -------------------------------------------------------------------------------- /Tests/ConformanceClient/Sources/CommandLineArgument.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // MARK: - Arguments 16 | 17 | enum ClientTypeArg: String, CaseIterable, CommandLineArgument { 18 | case swiftNIO = "nio" 19 | case urlSession = "urlsession" 20 | 21 | static let key = "httpclient" 22 | } 23 | 24 | // MARK: - Protocol interface 25 | 26 | protocol CommandLineArgument: RawRepresentable, CaseIterable { 27 | static var key: String { get } 28 | 29 | static func fromCommandLineArguments(_ arguments: [String]) throws -> Self 30 | } 31 | 32 | extension CommandLineArgument where RawValue == String { 33 | static func fromCommandLineArguments(_ arguments: [String]) throws -> Self { 34 | guard let argument = arguments.first(where: { $0.hasPrefix(self.key) }) else { 35 | throw "'\(self.key)' argument must be specified" 36 | } 37 | 38 | guard let argumentValue = self.init( 39 | rawValue: argument 40 | .replacingOccurrences(of: "\(self.key)=", with: "") 41 | .trimmingCharacters(in: .whitespaces) 42 | ) else { 43 | throw """ 44 | Invalid argument passed for '\(self.key)' argument. \ 45 | Expected \(self.allCases.map(\.rawValue)), got '\(argument)' 46 | """ 47 | } 48 | 49 | return argumentValue 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/ConformanceClient/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/apple/swift:v1.28.2 4 | opt: Visibility=Internal 5 | out: ./GeneratedSources 6 | - name: connect-swift 7 | opt: 8 | - GenerateServiceMetadata=false 9 | - Visibility=Internal 10 | out: ./GeneratedSources 11 | path: ../../.tmp/bin/protoc-gen-connect-swift 12 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/ConnectEndStreamResponseTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @testable import Connect 16 | import Foundation 17 | import XCTest 18 | 19 | final class ConnectEndStreamResponseTests: XCTestCase { 20 | func testLowercasesAllHeaderKeys() throws { 21 | let dictionary = [ 22 | "metadata": [ 23 | "sOmEkEy": ["foo"], 24 | "otherKey1": ["BAR", "bAz"], 25 | ], 26 | ] 27 | let data = try JSONSerialization.data(withJSONObject: dictionary) 28 | let response = try JSONDecoder().decode(ConnectEndStreamResponse.self, from: data) 29 | XCTAssertNil(response.error) 30 | XCTAssertEqual(response.metadata, ["somekey": ["foo"], "otherkey1": ["BAR", "bAz"]]) 31 | } 32 | 33 | func testAllowsOmittedErrorAndMetadata() throws { 34 | let data = try JSONSerialization.data(withJSONObject: [String: Any]()) 35 | let response = try JSONDecoder().decode(ConnectEndStreamResponse.self, from: data) 36 | XCTAssertNil(response.error) 37 | XCTAssertNil(response.metadata) 38 | } 39 | 40 | func testDeserializesError() throws { 41 | let dictionary = [ 42 | "error": [ 43 | "code": "permission_denied", 44 | ], 45 | ] 46 | let data = try JSONSerialization.data(withJSONObject: dictionary) 47 | let response = try JSONDecoder().decode(ConnectEndStreamResponse.self, from: data) 48 | XCTAssertEqual(response.error?.code, .permissionDenied) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @testable import Connect 16 | import XCTest 17 | 18 | final class EnvelopeTests: XCTestCase { 19 | func testPackingAndUnpackingCompressedMessage() throws { 20 | let originalData = Data(repeating: 0xa, count: 50) 21 | let packed = Envelope._packMessage( 22 | originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) 23 | ) 24 | let compressed = try GzipCompressionPool().compress(data: originalData) 25 | XCTAssertEqual(packed[0], 1) // Compression flag = true 26 | XCTAssertTrue(Envelope._isCompressed(packed)) 27 | XCTAssertEqual(Envelope._messageLength(forPackedData: packed), compressed.count) 28 | XCTAssertEqual(packed[5...], compressed) // Post-prefix data should match compressed value 29 | 30 | let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) 31 | XCTAssertEqual(unpacked.unpacked, originalData) 32 | XCTAssertEqual(unpacked.headerByte, 1) // Compression flag = true 33 | } 34 | 35 | func testPackingAndUnpackingUncompressedMessageBecauseCompressionMinBytesIsNil() throws { 36 | let originalData = Data(repeating: 0xa, count: 50) 37 | let packed = Envelope._packMessage(originalData, using: nil) 38 | XCTAssertEqual(packed[0], 0) // Compression flag = false 39 | XCTAssertFalse(Envelope._isCompressed(packed)) 40 | XCTAssertEqual(Envelope._messageLength(forPackedData: packed), originalData.count) 41 | XCTAssertEqual(packed[5...], originalData) // Post-prefix data should match compressed value 42 | 43 | // Compression pool should be ignored since the message is not compressed 44 | let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) 45 | XCTAssertEqual(unpacked.unpacked, originalData) 46 | XCTAssertEqual(unpacked.headerByte, 0) // Compression flag = false 47 | } 48 | 49 | func testPackingAndUnpackingUncompressedMessageBecauseMessageIsTooSmall() throws { 50 | let originalData = Data(repeating: 0xa, count: 50) 51 | let packed = Envelope._packMessage( 52 | originalData, using: .init(minBytes: 100, pool: GzipCompressionPool()) 53 | ) 54 | XCTAssertEqual(packed[0], 0) // Compression flag = false 55 | XCTAssertFalse(Envelope._isCompressed(packed)) 56 | XCTAssertEqual(Envelope._messageLength(forPackedData: packed), originalData.count) 57 | XCTAssertEqual(packed[5...], originalData) // Post-prefix data should match compressed value 58 | 59 | // Compression pool should be ignored since the message is not compressed 60 | let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) 61 | XCTAssertEqual(unpacked.unpacked, originalData) 62 | XCTAssertEqual(unpacked.headerByte, 0) // Compression flag = false 63 | } 64 | 65 | func testThrowsWhenUnpackingCompressedMessageWithoutDecompressionPool() throws { 66 | let originalData = Data(repeating: 0xa, count: 50) 67 | let packed = Envelope._packMessage( 68 | originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) 69 | ) 70 | let compressed = try GzipCompressionPool().compress(data: originalData) 71 | XCTAssertEqual(packed[0], 1) // Compression flag = true 72 | XCTAssertTrue(Envelope._isCompressed(packed)) 73 | XCTAssertEqual(Envelope._messageLength(forPackedData: packed), compressed.count) 74 | XCTAssertEqual(packed[5...], compressed) // Post-prefix data should match compressed value 75 | 76 | XCTAssertThrowsError(try Envelope._unpackMessage(packed, compressionPool: nil)) { error in 77 | XCTAssertEqual(error as? Envelope.Error, .missingExpectedCompressionPool) 78 | } 79 | } 80 | 81 | func testMessageLengthOfIncompleteData() { 82 | // Messages are incomplete if they do not contain enough data for the 5-byte prefix 83 | for length in 0..<5 { 84 | let data = Data(repeating: 0xa, count: length) 85 | XCTAssertEqual(Envelope._messageLength(forPackedData: data), -1) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/GzipCompressionPoolTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import Foundation 17 | import XCTest 18 | 19 | final class GzipCompressionPoolTests: XCTestCase { 20 | func testDecompressingGzippedFile() throws { 21 | let compressedData = try self.gzippedFileData() 22 | let decompressedText = try XCTUnwrap(String( 23 | data: try GzipCompressionPool().decompress(data: compressedData), 24 | encoding: .utf8 25 | )) 26 | XCTAssertEqual(decompressedText, self.expectedUnzippedFileText()) 27 | } 28 | 29 | func testCompressingAndDecompressingData() throws { 30 | let original = try XCTUnwrap(self.expectedUnzippedFileText().data(using: .utf8)) 31 | let compressionPool = GzipCompressionPool() 32 | let compressed = try compressionPool.compress(data: original) 33 | XCTAssertTrue(compressed.starts(with: [0x1f, 0x8b])) // Indicates gzip 34 | XCTAssertNotEqual(compressed.count, original.count) 35 | 36 | let decompressed = try compressionPool.decompress(data: compressed) 37 | XCTAssertEqual(decompressed, original) 38 | } 39 | 40 | func testDoesNotGzipDataThatIsAlreadyGzipped() throws { 41 | let compressed = try self.gzippedFileData() 42 | XCTAssertEqual(compressed, try GzipCompressionPool().compress(data: compressed)) 43 | } 44 | 45 | // MARK: - Private 46 | 47 | private func expectedUnzippedFileText() -> String { 48 | return "the quiccccccck brown fox juuuuuuuuuumps over the lazy doggggggggggggggg\n" 49 | } 50 | 51 | private func gzippedFileData() throws -> Data { 52 | let gzippedFileURL = try XCTUnwrap( 53 | Bundle.module.url( 54 | forResource: "gzip-test.txt.gz", withExtension: nil, subdirectory: "TestResources" 55 | ) 56 | ) 57 | return try Data(contentsOf: gzippedFileURL) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/InterceptorFactoryTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @testable import Connect 16 | import XCTest 17 | 18 | private final class MockUnaryInterceptor: UnaryInterceptor {} 19 | 20 | private final class MockStreamInterceptor: StreamInterceptor {} 21 | 22 | private final class MockUnaryAndStreamInterceptor: UnaryInterceptor, StreamInterceptor {} 23 | 24 | final class InterceptorFactoryTests: XCTestCase { 25 | private let config = ProtocolClientConfig(host: "localhost") 26 | 27 | func testInstantiatesUnaryInterceptorForUnary() { 28 | let factory = InterceptorFactory { _ in MockUnaryInterceptor() } 29 | XCTAssertTrue( 30 | factory.createUnary(with: self.config) is MockUnaryInterceptor 31 | ) 32 | } 33 | 34 | func testInstantiatesStreamInterceptorForStream() { 35 | let factory = InterceptorFactory { _ in MockStreamInterceptor() } 36 | XCTAssertTrue( 37 | factory.createStream(with: self.config) is MockStreamInterceptor 38 | ) 39 | } 40 | 41 | func testInstantiatesCombinedInterceptorForStreamAndUnary() { 42 | let factory = InterceptorFactory { _ in MockUnaryAndStreamInterceptor() } 43 | XCTAssertTrue( 44 | factory.createUnary(with: self.config) is MockUnaryAndStreamInterceptor 45 | ) 46 | XCTAssertTrue( 47 | factory.createStream(with: self.config) is MockUnaryAndStreamInterceptor 48 | ) 49 | } 50 | 51 | func testDoesNotInstantiateUnaryInterceptorForStream() { 52 | let factory = InterceptorFactory { _ in MockUnaryInterceptor() } 53 | XCTAssertNil(factory.createStream(with: self.config)) 54 | } 55 | 56 | func testDoesNotInstantiateStreamInterceptorForUnary() { 57 | let factory = InterceptorFactory { _ in MockStreamInterceptor() } 58 | XCTAssertNil(factory.createUnary(with: self.config)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/JSONCodecTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import Foundation 17 | import SwiftProtobuf 18 | import XCTest 19 | 20 | final class JSONCodecTests: XCTestCase { 21 | private let message: Connectrpc_Conformance_V1_RawHTTPRequest = .with { proto in 22 | proto.body = .unary(Connectrpc_Conformance_V1_MessageContents.with { message in 23 | message.binary = Data([0x0, 0x1, 0x2, 0x3]) 24 | message.compression = .gzip 25 | }) 26 | proto.uri = "foo/bar" 27 | proto.headers = [ 28 | .with { header in 29 | header.name = "x-header" 30 | header.value = ["a"] 31 | }, 32 | ] 33 | proto.rawQueryParams = [.init()] 34 | } 35 | 36 | func testSerializingAndDeserializingWithDefaultOptions() throws { 37 | let codec = JSONCodec() 38 | let serialized = try codec.serialize(message: self.message) 39 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 40 | } 41 | 42 | func testSerializingAndDeserializingWithEnumsAsInts() throws { 43 | let codec = JSONCodec(alwaysEncodeEnumsAsInts: true, preserveProtobufFieldNames: false) 44 | let serialized = try codec.serialize(message: self.message) 45 | let dictionary = try XCTUnwrap( 46 | try JSONSerialization.jsonObject(with: serialized) as? [String: Any] 47 | ) 48 | XCTAssertEqual((dictionary["unary"] as? [String: Any])?["compression"] as? Int, 2) 49 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 50 | } 51 | 52 | func testSerializingAndDeserializingWithEnumsAsStrings() throws { 53 | let codec = JSONCodec(alwaysEncodeEnumsAsInts: false, preserveProtobufFieldNames: false) 54 | let serialized = try codec.serialize(message: self.message) 55 | let dictionary = try XCTUnwrap( 56 | try JSONSerialization.jsonObject(with: serialized) as? [String: Any] 57 | ) 58 | XCTAssertEqual( 59 | (dictionary["unary"] as? [String: Any])?["compression"] as? String, "COMPRESSION_GZIP" 60 | ) 61 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 62 | } 63 | 64 | func testSerializingAndDeserializingWithProtobufFieldNames() throws { 65 | let codec = JSONCodec(alwaysEncodeEnumsAsInts: false, preserveProtobufFieldNames: true) 66 | let serialized = try codec.serialize(message: self.message) 67 | let dictionary = try XCTUnwrap( 68 | try JSONSerialization.jsonObject(with: serialized) as? [String: Any] 69 | ) 70 | XCTAssertTrue(Set(dictionary.keys).isSuperset(of: [ 71 | "headers", 72 | "unary", 73 | "uri", 74 | "raw_query_params", 75 | ])) 76 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 77 | } 78 | 79 | func testSerializingAndDeserializingWithCamelCaseFieldNames() throws { 80 | let codec = JSONCodec(alwaysEncodeEnumsAsInts: false, preserveProtobufFieldNames: false) 81 | let serialized = try codec.serialize(message: self.message) 82 | let dictionary = try XCTUnwrap( 83 | try JSONSerialization.jsonObject(with: serialized) as? [String: Any] 84 | ) 85 | XCTAssertTrue(Set(dictionary.keys).isSuperset(of: [ 86 | "headers", 87 | "unary", 88 | "uri", 89 | "rawQueryParams", 90 | ])) 91 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/ProtoCodecTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import SwiftProtobuf 17 | import XCTest 18 | 19 | final class ProtoCodecTests: XCTestCase { 20 | private let message: Connectrpc_Conformance_V1_RawHTTPRequest = .with { proto in 21 | proto.body = .unary(Connectrpc_Conformance_V1_MessageContents.with { message in 22 | message.binary = Data([0x0, 0x1, 0x2, 0x3]) 23 | }) 24 | proto.uri = "foo/bar" 25 | proto.headers = [ 26 | .with { header in 27 | header.name = "x-header" 28 | header.value = ["a"] 29 | }, 30 | ] 31 | } 32 | 33 | func testSerializingAndDeserializing() throws { 34 | let codec = ProtoCodec() 35 | let serialized = try codec.serialize(message: self.message) 36 | XCTAssertEqual(try codec.deserialize(source: serialized), self.message) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/ConnectTests/ServiceMetadataTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Connect 16 | import XCTest 17 | 18 | final class ServiceMetadataTests: XCTestCase { 19 | func testMethodSpecsAreGeneratedCorrectlyForService() { 20 | XCTAssertEqual( 21 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.unary, 22 | MethodSpec( 23 | name: "Unary", 24 | service: "connectrpc.conformance.v1.ConformanceService", 25 | type: .unary 26 | ) 27 | ) 28 | XCTAssertEqual( 29 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.unary.path, 30 | "connectrpc.conformance.v1.ConformanceService/Unary" 31 | ) 32 | 33 | XCTAssertEqual( 34 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.serverStream, 35 | MethodSpec( 36 | name: "ServerStream", 37 | service: "connectrpc.conformance.v1.ConformanceService", 38 | type: .serverStream 39 | ) 40 | ) 41 | XCTAssertEqual( 42 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.serverStream.path, 43 | "connectrpc.conformance.v1.ConformanceService/ServerStream" 44 | ) 45 | 46 | XCTAssertEqual( 47 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.clientStream, 48 | MethodSpec( 49 | name: "ClientStream", 50 | service: "connectrpc.conformance.v1.ConformanceService", 51 | type: .clientStream 52 | ) 53 | ) 54 | XCTAssertEqual( 55 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.clientStream.path, 56 | "connectrpc.conformance.v1.ConformanceService/ClientStream" 57 | ) 58 | 59 | XCTAssertEqual( 60 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.bidiStream, 61 | MethodSpec( 62 | name: "BidiStream", 63 | service: "connectrpc.conformance.v1.ConformanceService", 64 | type: .bidirectionalStream 65 | ) 66 | ) 67 | XCTAssertEqual( 68 | Connectrpc_Conformance_V1_ConformanceServiceClient.Metadata.Methods.bidiStream.path, 69 | "connectrpc.conformance.v1.ConformanceService/BidiStream" 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/TestResources/gzip-test.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-swift/7a67b10b04352717685a08aaf150a7dcf25a05ae/Tests/UnitTests/ConnectLibraryTests/TestResources/gzip-test.txt.gz -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectLibraryTests/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/apple/swift:v1.28.2 4 | opt: Visibility=Internal 5 | out: ./GeneratedSources 6 | - name: connect-swift 7 | opt: 8 | - GenerateAsyncMethods=true 9 | - GenerateCallbackMethods=true 10 | - Visibility=Internal 11 | out: ./GeneratedSources 12 | path: ../../../.tmp/bin/protoc-gen-connect-swift 13 | - name: connect-swift-mocks 14 | opt: 15 | - GenerateAsyncMethods=true 16 | - GenerateCallbackMethods=true 17 | - Visibility=Internal 18 | out: ./GeneratedSources 19 | path: ../../../.tmp/bin/protoc-gen-connect-swift-mocks 20 | -------------------------------------------------------------------------------- /Tests/UnitTests/ConnectPluginUtilitiesTests/FilePathComponentsTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @testable import ConnectPluginUtilities 16 | import Foundation 17 | import XCTest 18 | 19 | final class FilePathComponentsTests: XCTestCase { 20 | func testProtoFilePathWithLeadingSlash() { 21 | let components = FilePathComponents(path: "/foo/bar/baz.proto") 22 | XCTAssertEqual(components.directory, "/foo/bar") 23 | XCTAssertEqual(components.base, "baz") 24 | XCTAssertEqual(components.suffix, ".proto") 25 | XCTAssertEqual( 26 | components.outputFilePath(withExtension: ".connect.swift", using: .fullPath), 27 | "/foo/bar/baz.connect.swift" 28 | ) 29 | XCTAssertEqual( 30 | components.outputFilePath(withExtension: ".connect.swift", using: .dropPath), 31 | "baz.connect.swift" 32 | ) 33 | XCTAssertEqual( 34 | components.outputFilePath(withExtension: ".connect.swift", using: .pathToUnderscores), 35 | "foo_bar_baz.connect.swift" 36 | ) 37 | } 38 | 39 | func testProtoFilePathWithoutLeadingSlash() { 40 | let components = FilePathComponents(path: "foo/bar/baz.proto") 41 | XCTAssertEqual(components.directory, "foo/bar") 42 | XCTAssertEqual(components.base, "baz") 43 | XCTAssertEqual(components.suffix, ".proto") 44 | XCTAssertEqual( 45 | components.outputFilePath(withExtension: ".connect.swift", using: .fullPath), 46 | "foo/bar/baz.connect.swift" 47 | ) 48 | XCTAssertEqual( 49 | components.outputFilePath(withExtension: ".connect.swift", using: .dropPath), 50 | "baz.connect.swift" 51 | ) 52 | XCTAssertEqual( 53 | components.outputFilePath(withExtension: ".connect.swift", using: .pathToUnderscores), 54 | "foo_bar_baz.connect.swift" 55 | ) 56 | } 57 | 58 | func testProtoFilePathWithoutDirectoryOrLeadingSlash() { 59 | let components = FilePathComponents(path: "baz.proto") 60 | XCTAssertEqual(components.directory, "") 61 | XCTAssertEqual(components.base, "baz") 62 | XCTAssertEqual(components.suffix, ".proto") 63 | XCTAssertEqual( 64 | components.outputFilePath(withExtension: ".connect.swift", using: .fullPath), 65 | "baz.connect.swift" 66 | ) 67 | XCTAssertEqual( 68 | components.outputFilePath(withExtension: ".connect.swift", using: .dropPath), 69 | "baz.connect.swift" 70 | ) 71 | XCTAssertEqual( 72 | components.outputFilePath(withExtension: ".connect.swift", using: .pathToUnderscores), 73 | "baz.connect.swift" 74 | ) 75 | } 76 | 77 | func testProtoFilePathWithoutDirectoryButWithLeadingSlash() { 78 | let components = FilePathComponents(path: "/baz.proto") 79 | XCTAssertEqual(components.directory, "") 80 | XCTAssertEqual(components.base, "baz") 81 | XCTAssertEqual(components.suffix, ".proto") 82 | XCTAssertEqual( 83 | components.outputFilePath(withExtension: ".connect.swift", using: .fullPath), 84 | "baz.connect.swift" 85 | ) 86 | XCTAssertEqual( 87 | components.outputFilePath(withExtension: ".connect.swift", using: .dropPath), 88 | "baz.connect.swift" 89 | ) 90 | XCTAssertEqual( 91 | components.outputFilePath(withExtension: ".connect.swift", using: .pathToUnderscores), 92 | "baz.connect.swift" 93 | ) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/UnitTests/README.md: -------------------------------------------------------------------------------- 1 | # UnitTests 2 | 3 | This directory contains categorizations of `XCTestCase` classes to validate 4 | various library and plugin behaviors. 5 | --------------------------------------------------------------------------------