Compare commits

...

28 Commits

Author SHA1 Message Date
Rick Newton-Rogers 5a53247bc4
Correct Generating-stubs.md example JSON (#76)
I believe the example JSON is missing a comma so I inserted it; then
fixed up other JSON issues.
2025-07-08 16:20:35 +01:00
George Barnett 8125093686
Document how to create a public service with private implementation (#73)
Motivation:

Sometimes libraries want to vend a public gRPC service but without
exposing the generated types as part of the public API. This is easy to
achieve but non-obvious.

Modifications:

- Add an article explaining how to achieve this

Result:

Easier for users to learn how to do this
2025-06-04 10:20:45 +01:00
George Barnett 6bd6c50372
Fix some incorrect docs on protoc options (#71)
Motivation:

In #70 the protoc plugin got renamed, some of the docs weren't updated
appropriately.

Modifications:

- Update the docs

Result:

More accurate docs
2025-06-02 14:32:37 +00:00
George Barnett df605cde29
Move to grpc-swift-2 (#70)
Motivation:

To support incremental migration, v2 has moved to the 'grpc-swift-2'
package.

Modifications:

- Update dependencies
- Update generated code
- Re-baseline availability
- Rename 'protoc-gen-grpc-swift' to 'protoc-gen-grpc-swift-2'

Result:

Easier to migrate from gRPC Swift 1 to 2
2025-05-30 11:34:11 +01:00
George Barnett f0f110af87
Move availability inline (#68)
Motivation:

It's hard for packages to incrementally adopt gRPC with platforms in the
package manifest as it requires packages to include platforms in their
manifest or mint a new major version.

Modifications:

- Move platform availability onto source code
- Check annotations in CI

Result:

Easier to adopt
2025-05-16 12:11:25 +01:00
George Barnett c96dec7076
Support availability overrides (#67)
Motivation:

grpc-swift 2.2.0 generates code with `@available` annotations. Sometimes
it's necessary to raise these.

Modifications:

- Add an `Availability` option to the code generator which, if set at
least once overrides the default availability.
- Update docs

Result:

Availability can be configured using `protoc-gen-grpc-swift`
2025-05-15 10:12:00 +01:00
George Barnett fda21d444a
Set version info using info from the package context (#66)
Motivation:

The version for the package included in protoc-gen-grpc-swift is updated
manually and is currently incorrect. We can get the appropriate
information from the context provided by SwiftPM.

Modifications:

- Add a C shim module which provides a version string from the package
context

Result:

- Version string is kept up-to-date
- Resolves #60
2025-05-14 18:10:49 +01:00
George Barnett 43cede763f
Regenerate code for tests (#65)
Motivation:

The generated code used in tests is out of date. This happened because
of an update to grpc-swift and the GitHub actions job to check generated
code wasn't a required status check (but now is).

Modifications:

- Regenerate generated code

Result:

Generated code check passes
2025-05-14 11:28:02 +01:00
George Barnett 652582337f
Fix a few warnings (#63)
Motivation:

Swift 6.1 emits a few new warnings from Swift Testing

Modifications:

- Remove redundant `#require` macros
- Enable warnings as errors in CI
- Remove `-require-explicit-sendable` as it wasn't enabled correctly (it
requires `-Xfrontend` and is currently incompatible with
`-warnings-as-errors`)

Result:

Fewer warnings
2025-05-14 09:50:22 +00:00
George Barnett 364041d312
Update expected generated code (#64)
Motivation:

grpc-swift 2.2.0 adds availability annotations to generated code. Tests
are now failing because they aren't expecting the annotations.

Modifications:

- Update min required version
- Update expectations in tests

Result:

Tests pass
2025-05-14 09:37:30 +01:00
George Barnett a547cfec11
Fix typo in docs (#61)
Motivation:

The "PathToUnderscores" options was incorrectly spelled
"PathToUnderscore"
in the docs.

Motivation:

- Add the missing "s"

Result:

More accurate docs.
2025-04-29 11:26:53 +01:00
George Barnett ac5100ae48
Make GoogleRPCStatus Hashable (#62)
Motivation:

GoogleRPCStatus should be hashable: all of its properties already are.

Modifications:

- Add missing Hashable conformance
- Fix incorrect doc

Result:

GoogleRPCStatus is Hashable
2025-04-29 10:00:09 +00:00
Rick Newton-Rogers c47ee1b774
Enable Swift 6.1 jobs in CI (#59)
Motivation:

Swift 6.1 has been released, we should add it to our CI coverage.

Modifications:

Add additional Swift 6.1 jobs where appropriate in main.yml,
pull_request.yml

Result:

Improved test coverage.
2025-04-14 11:24:35 +01:00
George Barnett 5edbc18f51
Fix GoogleRPCStatus encoding/decoding (#58)
Motivation:

The 'rich' error model packs a google.rpc.status protobuf message into
the trailing metadata of an RPC. This should just be the base64 encoded
bytes of the serialzed message. At the moment this is packed within a
google.protobuf.any and then added to the metadata which doesn't interop
well with other languages.

Modifications:

- Remove the indirection
- Add methods for serializing/deserializing the RPC status wrapper

Result:

Better interop
2025-04-09 11:48:56 +01:00
Ethan 49bdd1232c
Add `swiftlint:disable all` to leading trivia (#53)
This line is also present on files generated by `swift-protobuf`.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2025-03-18 10:47:40 +00:00
Rick Newton-Rogers 6830cf0ea5
Add static SDK CI workflow (#55)
Add static SDK CI workflow which runs on commits to PRs, merges to main
and daily on main.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2025-03-17 16:50:05 +00:00
George Barnett 63982ca29f
Add command plugin to package products (#52)
Motivation:

The command plugin needs to be a product in order for it to be used.

Modifications:

- Add it to the products
- Give it the same name as the command, otherwise diagnostics (like
SwiftPM asking for permission to write to the package dir) will use a
different name which isn't obvious.
- Fix a few build warnings.

Result:

- Command plugin can be used
2025-03-05 18:24:06 +00:00
George Barnett 237ca5f91c
Don't generate reflection data. (#51)
Motivation:

v1 generated reflection data via the protoc plugin; this wasn't remvoed
from v2 but the v2 reflection servcice doesn't use data in the same
format. Instead, v2 expects users to invoke protoc directly to generate
a descriptor set. This makes the option redundant: it can be called but
the output can't be used.

Modifications:

- Remove the reflection data generating code and have the option throw
an error.

Result:

Less code
2025-03-05 16:04:43 +00:00
George Barnett 9cd9d4e618
Allow module names to be configured (#48)
Modifications:

- Give the protobuf generator its own config struct
- Use it in the protoc-gen-grpc-swift options
- Add new options for module names and propagate them through to the
config
- Use the custom module names where applicable

Result:

- Generated code can include different module name imports.

---------

Co-authored-by: Rick Newton-Rogers <rnro@apple.com>
2025-03-05 15:05:57 +00:00
Rick Newton-Rogers 8726dda0b5
Code generation command plugin (#40)
### Motivation:

To make it simpler to generate gRPC stubs with `protoc-gen-grpc-swift`
and `protoc-gen-swift`.

### Modifications:

* Add a new command plugin
* Refactor some errors

The command plugin can be invoked from the CLI as:
```
swift package generate-grpc-code-from-protos --import-path /path/to/Protos -- /path/to/Protos/HelloWorld.proto
```

The plugin has flexible configuration:
```
❯ swift package generate-grpc-code-from-protos --help
Usage: swift package generate-grpc-code-from-protos [flags] [--] [input files]

Flags:

  --servers                   Indicate that server code is to be generated. Generated by default.
  --no-servers                Indicate that server code is not to be generated. Generated by default.
  --clients                   Indicate that client code is to be generated. Generated by default.
  --no-clients                Indicate that client code is not to be generated. Generated by default.
  --messages                  Indicate that message code is to be generated. Generated by default.
  --no-messages               Indicate that message code is not to be generated. Generated by default.
  --file-naming               The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath.
  --access-level              The access level of the generated source [internal/public/package]. Defaults to internal.
  --access-level-on-imports   Whether imports should have explicit access levels. Defaults to false.
  --import-path               The directory in which to search for imports.
  --protoc-path               The path to the protoc binary.
  --output-path               The path into which the generated source files are created.
  --verbose                   Emit verbose output.
  --dry-run                   Print but do not execute the protoc commands.
  --help                      Print this help.
```
* When executing, the command prints the `protoc` invocations it uses
for ease of debugging. The `--dry-run` flag can be supplied for this
purpose or so that they may be extracted and used separately e.g. in a
script.
* If no `protoc` path is supplied then Swift Package Manager will
attempt to locate it.
* If no `output` directory is supplied then generated files are placed a
Swift Package Manager build directory.
  
### Result:

More convenient code generation

This PR is split out of
https://github.com/grpc/grpc-swift-protobuf/pull/26

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2025-03-05 10:35:22 +00:00
George Barnett 108c131067
Remove explicit 'GITHUB_ACTIONS=true' (#49)
Motivation:

It is no longer required.

Modifications:

Remove explicit 'GITHUB_ACTIONS=true' from workflows, it's now set
automatically.

Result:

Clearer config.
2025-03-04 15:51:39 +00:00
Rick Newton-Rogers 2ba29cc3ee
Rename nightly_6_1 params to nightly_next (#50)
Rename nightly_6_1 params to nightly_next; see
https://github.com/apple/swift-nio/pull/3122
2025-03-03 14:46:17 +00:00
George Barnett 64f97e95a8
Use path-to-underscore when generating files with the plugin (#47)
Motivation:

The build system can't compile the code generated by the plugin when
different proto files with the same name are used. This is allowed and
expected if the files are in different proto packages.

Modifications:

- Use the "path to underscore" naming scheme instead of the "full path"
  naming scheme. This changes path components to underscores, so
  "foo/bar/baz.proto" would have "foo_bar_baz.grpc.swift" generated for
  it.
- Uncomment test.

Result:

Test passes.
2025-02-24 17:03:54 +00:00
George Barnett e426ad8924
Add more plugin integration tests (#46)
Motivation:

The SwiftPM build plugin doesn't currently handle proto files with the
same name even if they come from different packages. This happens
because the build system cannot handle object files with the same name.
This change adds such a test while tidying up some of the plugin test
code.

Modifications:

- Divide up test resources in sources, config, and proto directories.
- Have one function per test
- Document each test with the expected structure
- Add another test but don't run it. (It fails.)

Result:

More tests. The issue will be fixed in a separate change.
2025-02-24 15:56:50 +00:00
George Barnett 7af6b403c5
Use local script for building plugins in tests (#45) 2025-02-18 16:16:05 +00:00
George Barnett bdbc8320f1
Relax dependency requirements (#43) 2025-01-31 14:11:37 +00:00
George Barnett d35611014e
Add docs explaining generated code and its API (#39)
Motivation:

Users should be able to understand how the generated code is structured
and how it may change over time.

Modifications:

Add two docs:
1. Explain the structure of the generate code and how to navigate it.
2. Explain how the generated code may change and hot to ensure it
doesn't cause API breakages.

Result:

Better docs
2025-01-30 13:25:38 +00:00
Rick Newton-Rogers 219783a584
CI use 6.1 nightlies (#42)
CI use 6.1 nightlies now that Swift development is happening in the 6.1
branch
2025-01-30 10:24:08 +00:00
58 changed files with 1876 additions and 451 deletions

View File

@ -13,9 +13,10 @@ jobs:
with: with:
linux_5_9_enabled: false linux_5_9_enabled: false
linux_5_10_enabled: false linux_5_10_enabled: false
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability"
construct-plugin-tests-matrix: construct-plugin-tests-matrix:
name: Construct plugin tests matrix name: Construct plugin tests matrix
@ -32,7 +33,7 @@ jobs:
env: env:
MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_9_ENABLED: false
MATRIX_LINUX_5_10_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false
MATRIX_LINUX_COMMAND: "curl -s https://raw.githubusercontent.com/rnro/grpc-swift-protobuf/refs/heads/build_plugin_integration_tests/dev/plugin-tests.sh | GITHUB_ACTIONS=true bash" MATRIX_LINUX_COMMAND: "./dev/plugin-tests.sh"
MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler" MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler"
plugin-tests-matrix: plugin-tests-matrix:
@ -42,3 +43,7 @@ jobs:
with: with:
name: "Plugin tests" name: "Plugin tests"
matrix_string: '${{ needs.construct-plugin-tests-matrix.outputs.plugin-tests-matrix }}' matrix_string: '${{ needs.construct-plugin-tests-matrix.outputs.plugin-tests-matrix }}'
static-sdk:
name: Static SDK
uses: apple/swift-nio/.github/workflows/static_sdk.yml@main

View File

@ -11,6 +11,9 @@ jobs:
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with: with:
license_header_check_project_name: "gRPC" license_header_check_project_name: "gRPC"
# This is done by a similar job defined in soundness.yml. It needs to be
# separate in order to export an environment variable.
api_breakage_check_enabled: false
grpc-soundness: grpc-soundness:
name: Soundness name: Soundness
@ -22,9 +25,10 @@ jobs:
with: with:
linux_5_9_enabled: false linux_5_9_enabled: false
linux_5_10_enabled: false linux_5_10_enabled: false
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability"
construct-plugin-tests-matrix: construct-plugin-tests-matrix:
name: Construct plugin tests matrix name: Construct plugin tests matrix
@ -41,7 +45,7 @@ jobs:
env: env:
MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_9_ENABLED: false
MATRIX_LINUX_5_10_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false
MATRIX_LINUX_COMMAND: "curl -s https://raw.githubusercontent.com/rnro/grpc-swift-protobuf/refs/heads/build_plugin_integration_tests/dev/plugin-tests.sh | GITHUB_ACTIONS=true bash" MATRIX_LINUX_COMMAND: "./dev/plugin-tests.sh"
MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler" MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler"
plugin-tests-matrix: plugin-tests-matrix:
@ -58,3 +62,7 @@ jobs:
with: with:
linux_5_9_enabled: false linux_5_9_enabled: false
linux_5_10_enabled: false linux_5_10_enabled: false
static-sdk:
name: Static SDK
uses: apple/swift-nio/.github/workflows/static_sdk.yml@main

View File

@ -35,3 +35,28 @@ jobs:
- name: Check generated code - name: Check generated code
run: | run: |
./dev/check-generated-code.sh ./dev/check-generated-code.sh
api-breakage-check:
name: API breakage check
runs-on: ubuntu-latest
container:
image: swift:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0 # Fetching tags requires fetch-depth: 0 (https://github.com/actions/checkout/issues/1471)
- name: Mark the workspace as safe
# https://github.com/actions/checkout/issues/766
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Run API breakage check
shell: bash
# See package.swift for why we set GRPC_SWIFT_PROTOBUF_NO_VERSION=1
run: |
export GRPC_SWIFT_PROTOBUF_NO_VERSION=1
git fetch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} ${GITHUB_BASE_REF}:pull-base-ref
BASELINE_REF='pull-base-ref'
echo "Using baseline: $BASELINE_REF"
swift package diagnose-api-breaking-changes "$BASELINE_REF"

View File

@ -0,0 +1,9 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package noop;
service NoOpService {
rpc NoOp(google.protobuf.Empty) returns (google.protobuf.Empty);
}

View File

@ -0,0 +1,9 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package noop2;
service NoOpService {
rpc NoOp(google.protobuf.Empty) returns (google.protobuf.Empty);
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2025, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import GRPCCore
import GRPCProtobuf
import SwiftProtobuf
@main
struct PluginAdopter {
static func main() async throws {
}
}
struct NoOp: Noop_NoOpService.SimpleServiceProtocol {
func noOp(
request: Google_Protobuf_Empty,
context: ServerContext
) async throws -> Google_Protobuf_Empty {
return Google_Protobuf_Empty()
}
}
struct NoOp2: Noop2_NoOpService.SimpleServiceProtocol {
func noOp(
request: Google_Protobuf_Empty,
context: ServerContext
) async throws -> Google_Protobuf_Empty {
return Google_Protobuf_Empty()
}
}

View File

@ -28,18 +28,17 @@ let package = Package(
], ],
dependencies: [ dependencies: [
// Dependency on grpc-swift-protobuf to be added by setup-plugin-tests.sh script // Dependency on grpc-swift-protobuf to be added by setup-plugin-tests.sh script
.package( .package(
url: "https://github.com/grpc/grpc-swift.git", url: "https://github.com/grpc/grpc-swift-2.git",
exact: "2.0.0-rc.1" from: "2.0.0"
) )
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "grpc-adopter", name: "grpc-adopter",
dependencies: [ dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"), .product(name: "GRPCInProcessTransport", package: "grpc-swift-2"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
], ],
plugins: [ plugins: [

View File

@ -23,19 +23,23 @@ let products: [Product] = [
targets: ["GRPCProtobuf"] targets: ["GRPCProtobuf"]
), ),
.executable( .executable(
name: "protoc-gen-grpc-swift", name: "protoc-gen-grpc-swift-2",
targets: ["protoc-gen-grpc-swift"] targets: ["protoc-gen-grpc-swift-2"]
), ),
.plugin( .plugin(
name: "GRPCProtobufGenerator", name: "GRPCProtobufGenerator",
targets: ["GRPCProtobufGenerator"] targets: ["GRPCProtobufGenerator"]
), ),
.plugin(
name: "generate-grpc-code-from-protos",
targets: ["generate-grpc-code-from-protos"]
),
] ]
let dependencies: [Package.Dependency] = [ let dependencies: [Package.Dependency] = [
.package( .package(
url: "https://github.com/grpc/grpc-swift.git", url: "https://github.com/grpc/grpc-swift-2.git",
exact: "2.0.0-rc.1" from: "2.0.0"
), ),
.package( .package(
url: "https://github.com/apple/swift-protobuf.git", url: "https://github.com/apple/swift-protobuf.git",
@ -43,20 +47,36 @@ let dependencies: [Package.Dependency] = [
), ),
] ]
let defaultSwiftSettings: [SwiftSetting] = [ // -------------------------------------------------------------------------------------------------
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
]
let targets: [Target] = [ // This adds some build settings which allow us to map "@available(gRPCSwiftProtobuf 2.x, *)" to
// protoc plugin for grpc-swift // the appropriate OS platforms.
let nextMinorVersion = 1
let availabilitySettings: [SwiftSetting] = (0 ... nextMinorVersion).map { minor in
let name = "gRPCSwiftProtobuf"
let version = "2.\(minor)"
let platforms = "macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"
let setting = "AvailabilityMacro=\(name) \(version):\(platforms)"
return .enableExperimentalFeature(setting)
}
let defaultSwiftSettings: [SwiftSetting] =
availabilitySettings + [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
]
// -------------------------------------------------------------------------------------------------
var targets: [Target] = [
// protoc plugin for grpc-swift-2
.executableTarget( .executableTarget(
name: "protoc-gen-grpc-swift", name: "protoc-gen-grpc-swift-2",
dependencies: [ dependencies: [
.target(name: "GRPCProtobufCodeGen"), .target(name: "GRPCProtobufCodeGen"),
.product(name: "GRPCCodeGen", package: "grpc-swift"), .product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"), .product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"), .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
], ],
@ -67,7 +87,7 @@ let targets: [Target] = [
.target( .target(
name: "GRPCProtobuf", name: "GRPCProtobuf",
dependencies: [ dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"), .product(name: "SwiftProtobuf", package: "swift-protobuf"),
], ],
swiftSettings: defaultSwiftSettings swiftSettings: defaultSwiftSettings
@ -76,18 +96,18 @@ let targets: [Target] = [
name: "GRPCProtobufTests", name: "GRPCProtobufTests",
dependencies: [ dependencies: [
.target(name: "GRPCProtobuf"), .target(name: "GRPCProtobuf"),
.product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"), .product(name: "GRPCInProcessTransport", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"), .product(name: "SwiftProtobuf", package: "swift-protobuf"),
], ],
swiftSettings: defaultSwiftSettings swiftSettings: defaultSwiftSettings
), ),
// Code generator library for protoc-gen-grpc-swift // Code generator library for protoc-gen-grpc-swift-2
.target( .target(
name: "GRPCProtobufCodeGen", name: "GRPCProtobufCodeGen",
dependencies: [ dependencies: [
.product(name: "GRPCCodeGen", package: "grpc-swift"), .product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"), .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
], ],
swiftSettings: defaultSwiftSettings swiftSettings: defaultSwiftSettings
@ -96,7 +116,7 @@ let targets: [Target] = [
name: "GRPCProtobufCodeGenTests", name: "GRPCProtobufCodeGenTests",
dependencies: [ dependencies: [
.target(name: "GRPCProtobufCodeGen"), .target(name: "GRPCProtobufCodeGen"),
.product(name: "GRPCCodeGen", package: "grpc-swift"), .product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"), .product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"), .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
], ],
@ -111,21 +131,81 @@ let targets: [Target] = [
name: "GRPCProtobufGenerator", name: "GRPCProtobufGenerator",
capability: .buildTool(), capability: .buildTool(),
dependencies: [ dependencies: [
.target(name: "protoc-gen-grpc-swift"), .target(name: "protoc-gen-grpc-swift-2"),
.product(name: "protoc-gen-swift", package: "swift-protobuf"), .product(name: "protoc-gen-swift", package: "swift-protobuf"),
] ]
), ),
// Code generator SwiftPM command
.plugin(
name: "generate-grpc-code-from-protos",
capability: .command(
intent: .custom(
verb: "generate-grpc-code-from-protos",
description: "Generate Swift code for gRPC services from protobuf definitions."
),
permissions: [
.writeToPackageDirectory(
reason:
"To write the generated Swift files back into the source directory of the package."
)
]
),
dependencies: [
.target(name: "protoc-gen-grpc-swift-2"),
.product(name: "protoc-gen-swift", package: "swift-protobuf"),
],
path: "Plugins/GRPCProtobufGeneratorCommand"
),
] ]
// -------------------------------------------------------------------------------------------------
extension Context {
fileprivate static var versionString: String {
guard let git = Self.gitInformation else { return "" }
if let tag = git.currentTag {
return tag
} else {
let suffix = git.hasUncommittedChanges ? " (modified)" : ""
return git.currentCommit + suffix
}
}
fileprivate static var buildCGRPCProtobuf: Bool {
let noVersion = Context.environment.keys.contains("GRPC_SWIFT_PROTOBUF_NO_VERSION")
return !noVersion
}
}
// Having a C module as a transitive dependency of a plugin seems to trip up the API breakage
// checking tool. See also https://github.com/swiftlang/swift-package-manager/issues/8081
//
// The CGRPCProtobuf module (which only includes package version information) is conditionally
// compiled and included based on an environment variable. This is set in CI only for the API
// breakage checking job to avoid tripping up SwiftPM.
if Context.buildCGRPCProtobuf {
targets.append(
.target(
name: "CGRPCProtobuf",
cSettings: [
.define("CGRPC_GRPC_SWIFT_PROTOBUF_VERSION", to: "\"\(Context.versionString)\"")
]
)
)
for target in targets {
if target.name == "protoc-gen-grpc-swift-2" {
target.dependencies.append(.target(name: "CGRPCProtobuf"))
}
}
}
// -------------------------------------------------------------------------------------------------
let package = Package( let package = Package(
name: "grpc-swift-protobuf", name: "grpc-swift-protobuf",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v11),
.visionOS(.v2),
],
products: products, products: products,
dependencies: dependencies, dependencies: dependencies,
targets: targets targets: targets

View File

@ -16,8 +16,6 @@
import Foundation import Foundation
let configFileName = "grpc-swift-proto-generator-config.json"
/// The config of the build plugin. /// The config of the build plugin.
struct BuildPluginConfig: Codable { struct BuildPluginConfig: Codable {
/// Config defining which components should be considered when generating source. /// Config defining which components should be considered when generating source.
@ -193,12 +191,14 @@ extension BuildPluginConfig.Protoc: Codable {
extension GenerationConfig { extension GenerationConfig {
init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) { init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) {
self.server = buildPluginConfig.generate.servers self.servers = buildPluginConfig.generate.servers
self.client = buildPluginConfig.generate.clients self.clients = buildPluginConfig.generate.clients
self.message = buildPluginConfig.generate.messages self.messages = buildPluginConfig.generate.messages
// hard-code full-path to avoid collisions since this goes into a temporary directory anyway // Use path to underscores as it ensures output files are unique (files generated from
self.fileNaming = .fullPath // "foo/bar.proto" won't collide with those generated from "bar/bar.proto" as they'll be
self.visibility = buildPluginConfig.generatedSource.accessLevel // uniquely named "foo_bar.(grpc|pb).swift" and "bar_bar.(grpc|pb).swift".
self.fileNaming = .pathToUnderscores
self.accessLevel = buildPluginConfig.generatedSource.accessLevel
self.accessLevelOnImports = buildPluginConfig.generatedSource.accessLevelOnImports self.accessLevelOnImports = buildPluginConfig.generatedSource.accessLevelOnImports
// Generate absolute paths for the imports relative to the config file in which they are specified // Generate absolute paths for the imports relative to the config file in which they are specified
self.importPaths = buildPluginConfig.protoc.importPaths.map { relativePath in self.importPaths = buildPluginConfig.protoc.importPaths.map { relativePath in

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2024, gRPC Authors All rights reserved. * Copyright 2025, gRPC Authors All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,13 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
enum PluginError: Error { enum BuildPluginError: Error {
// Build plugin
case incompatibleTarget(String) case incompatibleTarget(String)
case noConfigFilesFound case noConfigFilesFound
} }
extension PluginError: CustomStringConvertible { extension BuildPluginError: CustomStringConvertible {
var description: String { var description: String {
switch self { switch self {
case .incompatibleTarget(let target): case .incompatibleTarget(let target):

View File

@ -19,10 +19,9 @@ import PackagePlugin
// Entry-point when using Package manifest // Entry-point when using Package manifest
extension GRPCProtobufGenerator: BuildToolPlugin { extension GRPCProtobufGenerator: BuildToolPlugin {
/// Create build commands, the entry-point when using a Package manifest.
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
guard let swiftTarget = target as? SwiftSourceModuleTarget else { guard let swiftTarget = target as? SwiftSourceModuleTarget else {
throw PluginError.incompatibleTarget(target.name) throw BuildPluginError.incompatibleTarget(target.name)
} }
let configFiles = swiftTarget.sourceFiles(withSuffix: configFileName).map { $0.url } let configFiles = swiftTarget.sourceFiles(withSuffix: configFileName).map { $0.url }
let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url } let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url }
@ -41,7 +40,6 @@ import XcodeProjectPlugin
// Entry-point when using Xcode projects // Entry-point when using Xcode projects
extension GRPCProtobufGenerator: XcodeBuildToolPlugin { extension GRPCProtobufGenerator: XcodeBuildToolPlugin {
/// Create build commands, the entry-point when using an Xcode project.
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
let configFiles = target.inputFiles.filter { let configFiles = target.inputFiles.filter {
$0.url.lastPathComponent == configFileName $0.url.lastPathComponent == configFileName
@ -62,7 +60,7 @@ extension GRPCProtobufGenerator: XcodeBuildToolPlugin {
@main @main
struct GRPCProtobufGenerator { struct GRPCProtobufGenerator {
/// Build plugin code common to both invocation types: package manifest Xcode project /// Build plugin common code
func createBuildCommands( func createBuildCommands(
pluginWorkDirectory: URL, pluginWorkDirectory: URL,
tool: (String) throws -> PluginContext.Tool, tool: (String) throws -> PluginContext.Tool,
@ -72,13 +70,13 @@ struct GRPCProtobufGenerator {
) throws -> [Command] { ) throws -> [Command] {
let configs = try readConfigFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory) let configs = try readConfigFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory)
let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift-2").url
let protocGenSwiftPath = try tool("protoc-gen-swift").url let protocGenSwiftPath = try tool("protoc-gen-swift").url
var commands: [Command] = [] var commands: [Command] = []
for inputFile in inputFiles { for inputFile in inputFiles {
guard let (configFilePath, config) = configs.findApplicableConfig(for: inputFile) else { guard let (configFilePath, config) = configs.findApplicableConfig(for: inputFile) else {
throw PluginError.noConfigFilesFound throw BuildPluginError.noConfigFilesFound
} }
let protocPath = try deriveProtocPath(using: config, tool: tool) let protocPath = try deriveProtocPath(using: config, tool: tool)
@ -90,7 +88,7 @@ struct GRPCProtobufGenerator {
} }
// unless *explicitly* opted-out // unless *explicitly* opted-out
if config.client || config.server { if config.clients || config.servers {
let grpcCommand = try protocGenGRPCSwiftCommand( let grpcCommand = try protocGenGRPCSwiftCommand(
inputFile: inputFile, inputFile: inputFile,
config: config, config: config,
@ -104,7 +102,7 @@ struct GRPCProtobufGenerator {
} }
// unless *explicitly* opted-out // unless *explicitly* opted-out
if config.message { if config.messages {
let protoCommand = try protocGenSwiftCommand( let protoCommand = try protocGenSwiftCommand(
inputFile: inputFile, inputFile: inputFile,
config: config, config: config,
@ -167,16 +165,16 @@ extension [URL: GenerationConfig] {
} }
} }
/// Construct the command to invoke `protoc` with the `protoc-gen-grpc-swift` plugin. /// Construct the command to invoke `protoc` with the `protoc-gen-grpc-swift-2` plugin.
/// - Parameters: /// - Parameters:
/// - inputFile: The input `.proto` file. /// - inputFile: The input `.proto` file.
/// - config: The config for this operation. /// - config: The config for this operation.
/// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
/// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files. /// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files.
/// - protocPath: The path to `protoc` /// - protocPath: The path to `protoc`
/// - protocGenGRPCSwiftPath: The path to `protoc-gen-grpc-swift`. /// - protocGenGRPCSwiftPath: The path to `protoc-gen-grpc-swift-2`.
/// - configFilePath: The path to the config file in use. /// - configFilePath: The path to the config file in use.
/// - Returns: The command to invoke `protoc` with the `protoc-gen-grpc-swift` plugin. /// - Returns: The command to invoke `protoc` with the `protoc-gen-grpc-swift-2` plugin.
func protocGenGRPCSwiftCommand( func protocGenGRPCSwiftCommand(
inputFile: URL, inputFile: URL,
config: GenerationConfig, config: GenerationConfig,
@ -189,7 +187,7 @@ func protocGenGRPCSwiftCommand(
let outputPathURL = URL(fileURLWithPath: config.outputPath) let outputPathURL = URL(fileURLWithPath: config.outputPath)
let outputFilePath = deriveOutputFilePath( let outputFilePath = deriveOutputFilePath(
for: inputFile, protoFile: inputFile,
baseDirectoryPath: baseDirectoryPath, baseDirectoryPath: baseDirectoryPath,
outputDirectory: outputPathURL, outputDirectory: outputPathURL,
outputExtension: "grpc.swift" outputExtension: "grpc.swift"
@ -224,7 +222,7 @@ func protocGenGRPCSwiftCommand(
/// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
/// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files. /// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files.
/// - protocPath: The path to `protoc` /// - protocPath: The path to `protoc`
/// - protocGenSwiftPath: The path to `protoc-gen-grpc-swift`. /// - protocGenSwiftPath: The path to `protoc-gen-grpc-swift-2`.
/// - configFilePath: The path to the config file in use. /// - configFilePath: The path to the config file in use.
/// - Returns: The command to invoke `protoc` with the `protoc-gen-swift` plugin. /// - Returns: The command to invoke `protoc` with the `protoc-gen-swift` plugin.
func protocGenSwiftCommand( func protocGenSwiftCommand(
@ -239,7 +237,7 @@ func protocGenSwiftCommand(
let outputPathURL = URL(fileURLWithPath: config.outputPath) let outputPathURL = URL(fileURLWithPath: config.outputPath)
let outputFilePath = deriveOutputFilePath( let outputFilePath = deriveOutputFilePath(
for: inputFile, protoFile: inputFile,
baseDirectoryPath: baseDirectoryPath, baseDirectoryPath: baseDirectoryPath,
outputDirectory: outputPathURL, outputDirectory: outputPathURL,
outputExtension: "pb.swift" outputExtension: "pb.swift"
@ -267,28 +265,32 @@ func protocGenSwiftCommand(
) )
} }
/// Derive the expected output file path to match the behavior of the `protoc-gen-swift` and `protoc-gen-grpc-swift` `protoc` plugins /// Derive the expected output file path to match the behavior of the `protoc-gen-swift`
/// when using the `FullPath` naming scheme. /// and `protoc-gen-grpc-swift-2` `protoc` plugins using the `PathToUnderscores` naming scheme.
///
/// This means the generated file for an input proto file called "foo/bar/baz.proto" will
/// have the name "foo\_bar\_baz.proto".
///
/// - Parameters: /// - Parameters:
/// - inputFile: The input `.proto` file. /// - protoFile: The path of the input `.proto` file.
/// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for
/// relative path naming schemes.
/// - outputDirectory: The directory in which generated source files are created. /// - outputDirectory: The directory in which generated source files are created.
/// - outputExtension: The file extension to be appended to generated files in-place of `.proto`. /// - outputExtension: The file extension to be appended to generated files in-place of `.proto`.
/// - Returns: The expected output file path. /// - Returns: The expected output file path.
func deriveOutputFilePath( func deriveOutputFilePath(
for inputFile: URL, protoFile: URL,
baseDirectoryPath: URL, baseDirectoryPath: URL,
outputDirectory: URL, outputDirectory: URL,
outputExtension: String outputExtension: String
) -> URL { ) -> URL {
// The name of the output file is based on the name of the input file. // Replace the extension (".proto") with the new extension (".grpc.swift"
// We validated in the beginning that every file has the suffix of .proto // or ".pb.swift").
// This means we can just drop the last 5 elements and append the new suffix precondition(protoFile.pathExtension == "proto")
let lastPathComponentRoot = inputFile.lastPathComponent.dropLast(5) let fileName = String(protoFile.lastPathComponent.dropLast(5) + outputExtension)
let lastPathComponent = String(lastPathComponentRoot + outputExtension)
// find the inputFile path relative to the proto directory // find the inputFile path relative to the proto directory
var relativePathComponents = inputFile.deletingLastPathComponent().pathComponents var relativePathComponents = protoFile.deletingLastPathComponent().pathComponents
for protoDirectoryPathComponent in baseDirectoryPath.pathComponents { for protoDirectoryPathComponent in baseDirectoryPath.pathComponents {
if relativePathComponents.first == protoDirectoryPathComponent { if relativePathComponents.first == protoDirectoryPathComponent {
relativePathComponents.removeFirst() relativePathComponents.removeFirst()
@ -297,10 +299,7 @@ func deriveOutputFilePath(
} }
} }
let outputFileComponents = relativePathComponents + [lastPathComponent] relativePathComponents.append(fileName)
var outputFilePath = outputDirectory let path = relativePathComponents.joined(separator: "_")
for outputFileComponent in outputFileComponents { return outputDirectory.appending(path: path)
outputFilePath.append(component: outputFileComponent)
}
return outputFilePath
} }

View File

@ -0,0 +1,255 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
import PackagePlugin
struct CommandConfig {
var common: GenerationConfig
var verbose: Bool
var dryRun: Bool
static let defaults = Self(
common: .init(
accessLevel: .internal,
servers: true,
clients: true,
messages: true,
fileNaming: .fullPath,
accessLevelOnImports: false,
importPaths: [],
outputPath: ""
),
verbose: false,
dryRun: false
)
static let parameterGroupSeparator = "--"
}
extension CommandConfig {
static func parse(
argumentExtractor argExtractor: inout ArgumentExtractor,
pluginWorkDirectory: URL
) throws -> CommandConfig {
var config = CommandConfig.defaults
for flag in OptionsAndFlags.allCases {
switch flag {
case .accessLevel:
if let value = argExtractor.extractSingleOption(named: flag.rawValue) {
if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) {
config.common.accessLevel = accessLevel
} else {
throw CommandPluginError.unknownAccessLevel(value)
}
}
case .noServers:
// Handled by `.servers`
continue
case .servers:
let servers = argExtractor.extractFlag(named: OptionsAndFlags.servers.rawValue)
let noServers = argExtractor.extractFlag(named: OptionsAndFlags.noServers.rawValue)
if servers > 0 && noServers > 0 {
throw CommandPluginError.conflictingFlags(
OptionsAndFlags.servers.rawValue,
OptionsAndFlags.noServers.rawValue
)
} else if servers > 0 {
config.common.servers = true
} else if noServers > 0 {
config.common.servers = false
}
case .noClients:
// Handled by `.clients`
continue
case .clients:
let clients = argExtractor.extractFlag(named: OptionsAndFlags.clients.rawValue)
let noClients = argExtractor.extractFlag(named: OptionsAndFlags.noClients.rawValue)
if clients > 0 && noClients > 0 {
throw CommandPluginError.conflictingFlags(
OptionsAndFlags.clients.rawValue,
OptionsAndFlags.noClients.rawValue
)
} else if clients > 0 {
config.common.clients = true
} else if noClients > 0 {
config.common.clients = false
}
case .noMessages:
// Handled by `.messages`
continue
case .messages:
let messages = argExtractor.extractFlag(named: OptionsAndFlags.messages.rawValue)
let noMessages = argExtractor.extractFlag(named: OptionsAndFlags.noMessages.rawValue)
if messages > 0 && noMessages > 0 {
throw CommandPluginError.conflictingFlags(
OptionsAndFlags.messages.rawValue,
OptionsAndFlags.noMessages.rawValue
)
} else if messages > 0 {
config.common.messages = true
} else if noMessages > 0 {
config.common.messages = false
}
case .fileNaming:
if let value = argExtractor.extractSingleOption(named: flag.rawValue) {
if let fileNaming = GenerationConfig.FileNaming(rawValue: value) {
config.common.fileNaming = fileNaming
} else {
throw CommandPluginError.unknownFileNamingStrategy(value)
}
}
case .accessLevelOnImports:
if argExtractor.extractFlag(named: flag.rawValue) > 0 {
config.common.accessLevelOnImports = true
}
case .importPath:
config.common.importPaths = argExtractor.extractOption(named: flag.rawValue)
case .protocPath:
config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue)
case .outputPath:
config.common.outputPath =
argExtractor.extractSingleOption(named: flag.rawValue)
?? pluginWorkDirectory.absoluteStringNoScheme
case .verbose:
let verbose = argExtractor.extractFlag(named: flag.rawValue)
config.verbose = verbose != 0
case .dryRun:
let dryRun = argExtractor.extractFlag(named: flag.rawValue)
config.dryRun = dryRun != 0
case .help:
() // handled elsewhere
}
}
if let argument = argExtractor.remainingArguments.first {
throw CommandPluginError.unknownOption(argument)
}
return config
}
}
extension ArgumentExtractor {
mutating func extractSingleOption(named optionName: String) -> String? {
let values = self.extractOption(named: optionName)
if values.count > 1 {
Diagnostics.warning(
"'--\(optionName)' was unexpectedly repeated, the first value will be used."
)
}
return values.first
}
}
/// All valid input options/flags
enum OptionsAndFlags: String, CaseIterable {
case servers
case noServers = "no-servers"
case clients
case noClients = "no-clients"
case messages
case noMessages = "no-messages"
case fileNaming = "file-naming"
case accessLevel = "access-level"
case accessLevelOnImports = "access-level-on-imports"
case importPath = "import-path"
case protocPath = "protoc-path"
case outputPath = "output-path"
case verbose
case dryRun = "dry-run"
case help
}
extension OptionsAndFlags {
func usageDescription() -> String {
switch self {
case .servers:
return "Generate server code. Generated by default."
case .noServers:
return "Do not generate server code. Generated by default."
case .clients:
return "Generate client code. Generated by default."
case .noClients:
return "Do not generate client code. Generated by default."
case .messages:
return "Generate message code. Generated by default."
case .noMessages:
return "Do not generate message code. Generated by default."
case .fileNaming:
return
"The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath."
case .accessLevel:
return
"The access level of the generated source [internal/public/package]. Defaults to internal."
case .accessLevelOnImports:
return "Whether imports should have explicit access levels. Defaults to false."
case .importPath:
return
"The directory in which to search for imports. May be specified multiple times. If none are specified the current working directory is used."
case .protocPath:
return "The path to the protoc binary."
case .dryRun:
return "Print but do not execute the protoc commands."
case .outputPath:
return "The directory into which the generated source files are created."
case .verbose:
return "Emit verbose output."
case .help:
return "Print this help."
}
}
static func printHelp(requested: Bool) {
let printMessage: (String) -> Void
if requested {
printMessage = { message in print(message) }
} else {
printMessage = Stderr.print
}
printMessage(
"Usage: swift package generate-grpc-code-from-protos [flags] [\(CommandConfig.parameterGroupSeparator)] [input files]"
)
printMessage("")
printMessage("Flags:")
printMessage("")
let spacing = 3
let maxLength =
(OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0)
+ spacing
for flag in OptionsAndFlags.allCases {
printMessage(
" --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())"
)
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2025, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
enum CommandPluginError: Error {
case invalidArgumentValue(name: String, value: String)
case missingInputFile
case unknownOption(String)
case unknownAccessLevel(String)
case unknownFileNamingStrategy(String)
case conflictingFlags(String, String)
case generationFailure(
errorDescription: String,
executable: String,
arguments: [String],
stdErr: String?
)
case tooManyParameterSeparators
}
extension CommandPluginError: CustomStringConvertible {
var description: String {
switch self {
case .invalidArgumentValue(let name, let value):
return "Invalid value '\(value)', for '\(name)'."
case .missingInputFile:
return "No input file(s) specified."
case .unknownOption(let name):
return "Provided option is unknown: \(name)."
case .unknownAccessLevel(let value):
return "Provided access level is unknown: \(value)."
case .unknownFileNamingStrategy(let value):
return "Provided file naming strategy is unknown: \(value)."
case .conflictingFlags(let flag1, let flag2):
return "Provided flags conflict: '\(flag1)' and '\(flag2)'."
case .generationFailure(let errorDescription, let executable, let arguments, let stdErr):
var message = """
Code generation failed with: \(errorDescription).
\tExecutable: \(executable)
\tArguments: \(arguments.joined(separator: " "))
"""
if let stdErr {
message += """
\n\tprotoc error output:
\t\(stdErr)
"""
}
return message
case .tooManyParameterSeparators:
return "Unexpected parameter structure, too many '--' separators."
}
}
}

View File

@ -0,0 +1,243 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
import PackagePlugin
extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
try self.performCommand(
arguments: arguments,
tool: context.tool,
pluginWorkDirectoryURL: context.pluginWorkDirectoryURL
)
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
// Entry-point when using Xcode projects
extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin {
func performCommand(context: XcodeProjectPlugin.XcodePluginContext, arguments: [String]) throws {
try self.performCommand(
arguments: arguments,
tool: context.tool,
pluginWorkDirectoryURL: context.pluginWorkDirectoryURL
)
}
}
#endif
@main
struct GRPCProtobufGeneratorCommandPlugin {
/// Command plugin common code
func performCommand(
arguments: [String],
tool: (String) throws -> PluginContext.Tool,
pluginWorkDirectoryURL: URL
) throws {
let flagsAndOptions: [String]
let inputFiles: [String]
let separatorCount = arguments.count { $0 == CommandConfig.parameterGroupSeparator }
switch separatorCount {
case 0:
var argExtractor = ArgumentExtractor(arguments)
// check if help requested
if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
OptionsAndFlags.printHelp(requested: true)
return
}
inputFiles = arguments
flagsAndOptions = []
case 1:
let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)!
flagsAndOptions = Array(arguments[..<splitIndex])
inputFiles = Array(arguments[splitIndex.advanced(by: 1)...])
default:
throw CommandPluginError.tooManyParameterSeparators
}
var argExtractor = ArgumentExtractor(flagsAndOptions)
// help requested
if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
OptionsAndFlags.printHelp(requested: true)
return
}
// MARK: Configuration
let commandConfig: CommandConfig
do {
commandConfig = try CommandConfig.parse(
argumentExtractor: &argExtractor,
pluginWorkDirectory: pluginWorkDirectoryURL
)
} catch {
throw error
}
if commandConfig.verbose {
Stderr.print("InputFiles: \(inputFiles.joined(separator: ", "))")
}
let config = commandConfig.common
let protocPath = try deriveProtocPath(using: config, tool: tool)
let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift-2").url
let protocGenSwiftPath = try tool("protoc-gen-swift").url
let outputDirectory = URL(fileURLWithPath: config.outputPath)
if commandConfig.verbose {
Stderr.print(
"Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'"
)
}
let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) }
// MARK: protoc-gen-grpc-swift-2
if config.clients || config.servers {
let arguments = constructProtocGenGRPCSwiftArguments(
config: config,
fileNaming: config.fileNaming,
inputFiles: inputFileURLs,
protoDirectoryPaths: config.importPaths,
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
outputDirectory: outputDirectory
)
try executeProtocInvocation(
executableURL: protocPath,
arguments: arguments,
verbose: commandConfig.verbose,
dryRun: commandConfig.dryRun
)
if !commandConfig.dryRun, commandConfig.verbose {
Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).")
}
}
// MARK: protoc-gen-swift
if config.messages {
let arguments = constructProtocGenSwiftArguments(
config: config,
fileNaming: config.fileNaming,
inputFiles: inputFileURLs,
protoDirectoryPaths: config.importPaths,
protocGenSwiftPath: protocGenSwiftPath,
outputDirectory: outputDirectory
)
try executeProtocInvocation(
executableURL: protocPath,
arguments: arguments,
verbose: commandConfig.verbose,
dryRun: commandConfig.dryRun
)
if !commandConfig.dryRun, commandConfig.verbose {
Stderr.print(
"Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))."
)
}
}
}
}
/// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation
/// - Parameters:
/// - executableURL: The path to the `protoc` executable.
/// - arguments: The arguments to be passed to `protoc`.
/// - verbose: Whether or not to print verbose output
/// - dryRun: If this invocation is a dry-run, i.e. will not actually be executed
func executeProtocInvocation(
executableURL: URL,
arguments: [String],
verbose: Bool,
dryRun: Bool
) throws {
if verbose {
Stderr.print("\(executableURL.absoluteStringNoScheme) \\")
Stderr.print(" \(arguments.joined(separator: " \\\n "))")
}
if dryRun {
return
}
let process = Process()
process.executableURL = executableURL
process.arguments = arguments
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
do {
try process.run()
} catch {
try printProtocOutput(outputPipe, verbose: verbose)
let stdErr: String?
if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
stdErr = String(decoding: errorData, as: UTF8.self)
} else {
stdErr = nil
}
throw CommandPluginError.generationFailure(
errorDescription: "\(error)",
executable: executableURL.absoluteStringNoScheme,
arguments: arguments,
stdErr: stdErr
)
}
process.waitUntilExit()
try printProtocOutput(outputPipe, verbose: verbose)
if process.terminationReason == .exit && process.terminationStatus == 0 {
return
}
let stdErr: String?
if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
stdErr = String(decoding: errorData, as: UTF8.self)
} else {
stdErr = nil
}
let problem = "\(process.terminationReason):\(process.terminationStatus)"
throw CommandPluginError.generationFailure(
errorDescription: problem,
executable: executableURL.absoluteStringNoScheme,
arguments: arguments,
stdErr: stdErr
)
}
func printProtocOutput(_ stdOut: Pipe, verbose: Bool) throws {
if verbose, let outputData = try stdOut.fileHandleForReading.readToEnd() {
let output = String(decoding: outputData, as: UTF8.self)
let lines = output.split { $0.isNewline }
print("protoc output:")
for line in lines {
print("\t\(line)")
}
}
}

View File

@ -0,0 +1 @@
../PluginsShared

View File

@ -32,7 +32,7 @@ struct GenerationConfig {
/// - `FullPath`: `foo/bar/baz.grpc.swift` /// - `FullPath`: `foo/bar/baz.grpc.swift`
/// - `PathToUnderscore`: `foo_bar_baz.grpc.swift` /// - `PathToUnderscore`: `foo_bar_baz.grpc.swift`
/// - `DropPath`: `baz.grpc.swift` /// - `DropPath`: `baz.grpc.swift`
enum FileNaming: String, Codable { enum FileNaming: String {
/// Replicate the input file path with the output file(s). /// Replicate the input file path with the output file(s).
case fullPath = "FullPath" case fullPath = "FullPath"
/// Convert path directory delimiters to underscores. /// Convert path directory delimiters to underscores.
@ -42,13 +42,13 @@ struct GenerationConfig {
} }
/// The visibility of the generated files. /// The visibility of the generated files.
var visibility: AccessLevel var accessLevel: AccessLevel
/// Whether server code is generated. /// Whether server code is generated.
var server: Bool var servers: Bool
/// Whether client code is generated. /// Whether client code is generated.
var client: Bool var clients: Bool
/// Whether message code is generated. /// Whether message code is generated.
var message: Bool var messages: Bool
/// The naming of output files with respect to the path of the source file. /// The naming of output files with respect to the path of the source file.
var fileNaming: FileNaming var fileNaming: FileNaming
/// Whether imports should have explicit access levels. /// Whether imports should have explicit access levels.
@ -83,3 +83,18 @@ extension GenerationConfig.AccessLevel: Codable {
} }
} }
} }
extension GenerationConfig.FileNaming: Codable {
init?(rawValue: String) {
switch rawValue.lowercased() {
case "fullpath":
self = .fullPath
case "pathtounderscores":
self = .pathToUnderscores
case "droppath":
self = .dropPath
default:
return nil
}
}
}

View File

@ -17,6 +17,8 @@
import Foundation import Foundation
import PackagePlugin import PackagePlugin
let configFileName = "grpc-swift-proto-generator-config.json"
/// Derive the path to the instance of `protoc` to be used. /// Derive the path to the instance of `protoc` to be used.
/// - Parameters: /// - Parameters:
/// - config: The supplied config. If no path is supplied then one is discovered using the `PROTOC_PATH` environment variable or the `findTool`. /// - config: The supplied config. If no path is supplied then one is discovered using the `PROTOC_PATH` environment variable or the `findTool`.
@ -63,7 +65,7 @@ func constructProtocGenSwiftArguments(
protocArgs.append("--proto_path=\(path)") protocArgs.append("--proto_path=\(path)")
} }
protocArgs.append("--swift_opt=Visibility=\(config.visibility.rawValue)") protocArgs.append("--swift_opt=Visibility=\(config.accessLevel.rawValue)")
protocArgs.append("--swift_opt=FileNaming=\(config.fileNaming.rawValue)") protocArgs.append("--swift_opt=FileNaming=\(config.fileNaming.rawValue)")
protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)") protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)")
protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme }) protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme })
@ -71,15 +73,15 @@ func constructProtocGenSwiftArguments(
return protocArgs return protocArgs
} }
/// Construct the arguments to be passed to `protoc` when invoking the `protoc-gen-grpc-swift` `protoc` plugin. /// Construct the arguments to be passed to `protoc` when invoking the `protoc-gen-grpc-swift-2` `protoc` plugin.
/// - Parameters: /// - Parameters:
/// - config: The config for this operation. /// - config: The config for this operation.
/// - fileNaming: The file naming scheme to be used. /// - fileNaming: The file naming scheme to be used.
/// - inputFiles: The input `.proto` files. /// - inputFiles: The input `.proto` files.
/// - protoDirectoryPaths: The directories in which `protoc` will look for imports. /// - protoDirectoryPaths: The directories in which `protoc` will look for imports.
/// - protocGenGRPCSwiftPath: The path to the `protoc-gen-grpc-swift` `protoc` plugin. /// - protocGenGRPCSwiftPath: The path to the `protoc-gen-grpc-swift-2` `protoc` plugin.
/// - outputDirectory: The directory in which generated source files are created. /// - outputDirectory: The directory in which generated source files are created.
/// - Returns: The constructed arguments to be passed to `protoc` when invoking the `protoc-gen-grpc-swift` `protoc` plugin. /// - Returns: The constructed arguments to be passed to `protoc` when invoking the `protoc-gen-grpc-swift-2` `protoc` plugin.
func constructProtocGenGRPCSwiftArguments( func constructProtocGenGRPCSwiftArguments(
config: GenerationConfig, config: GenerationConfig,
fileNaming: GenerationConfig.FileNaming?, fileNaming: GenerationConfig.FileNaming?,
@ -97,9 +99,9 @@ func constructProtocGenGRPCSwiftArguments(
protocArgs.append("--proto_path=\(path)") protocArgs.append("--proto_path=\(path)")
} }
protocArgs.append("--grpc-swift_opt=Visibility=\(config.visibility.rawValue.capitalized)") protocArgs.append("--grpc-swift_opt=Visibility=\(config.accessLevel.rawValue.capitalized)")
protocArgs.append("--grpc-swift_opt=Server=\(config.server)") protocArgs.append("--grpc-swift_opt=Server=\(config.servers)")
protocArgs.append("--grpc-swift_opt=Client=\(config.client)") protocArgs.append("--grpc-swift_opt=Client=\(config.clients)")
protocArgs.append("--grpc-swift_opt=FileNaming=\(config.fileNaming.rawValue)") protocArgs.append("--grpc-swift_opt=FileNaming=\(config.fileNaming.rawValue)")
protocArgs.append("--grpc-swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)") protocArgs.append("--grpc-swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)")
protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme }) protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme })
@ -117,3 +119,14 @@ extension URL {
return absoluteString return absoluteString
} }
} }
enum Stderr {
private static let newLine = "\n".data(using: .utf8)!
static func print(_ message: String) {
if let data = message.data(using: .utf8) {
FileHandle.standardError.write(data)
FileHandle.standardError.write(Self.newLine)
}
}
}

View File

@ -14,5 +14,5 @@ for [gRPC Swift][gh-grpc-swift-protobuf].
[gh-swift-protobuf]: https://github.com/apple/swift-protobuf [gh-swift-protobuf]: https://github.com/apple/swift-protobuf
[gh-grpc-swift-protobuf]: https://github.com/grpc/grpc-swift-protobuf [gh-grpc-swift-protobuf]: https://github.com/grpc/grpc-swift-protobuf
[spi-grpc-swift]: https://swiftpackageindex.com/grpc/grpc-swift/documentation [spi-grpc-swift]: https://swiftpackageindex.com/grpc/grpc-swift-2/documentation
[spi-grpc-swift-protobuf]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation [spi-grpc-swift-protobuf]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation

View File

@ -0,0 +1,19 @@
// Copyright 2025, gRPC Authors All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "CGRPCProtobuf.h"
const char *cgrprc_grpc_swift_protobuf_version() {
return CGRPC_GRPC_SWIFT_PROTOBUF_VERSION;
}

View File

@ -0,0 +1,20 @@
// Copyright 2025, gRPC Authors All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef CGRPC_PROTOBUF_H_
#define CGRPC_PROTOBUF_H_
const char *cgrprc_grpc_swift_protobuf_version();
#endif // CGRPC_PROTOBUF_H_

View File

@ -18,6 +18,7 @@ public import GRPCCore
public import SwiftProtobuf public import SwiftProtobuf
/// Serializes a Protobuf message into a sequence of bytes. /// Serializes a Protobuf message into a sequence of bytes.
@available(gRPCSwiftProtobuf 2.0, *)
public struct ProtobufSerializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageSerializer { public struct ProtobufSerializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageSerializer {
public init() {} public init() {}
@ -41,6 +42,7 @@ public struct ProtobufSerializer<Message: SwiftProtobuf.Message>: GRPCCore.Messa
} }
/// Deserializes a sequence of bytes into a Protobuf message. /// Deserializes a sequence of bytes into a Protobuf message.
@available(gRPCSwiftProtobuf 2.0, *)
public struct ProtobufDeserializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageDeserializer { public struct ProtobufDeserializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageDeserializer {
public init() {} public init() {}

View File

@ -24,6 +24,7 @@ public import SwiftProtobuf // internal but @usableFromInline
/// it'd require a dependency on Protobuf in the core package), and `GRPCContiguousBytes` can't /// it'd require a dependency on Protobuf in the core package), and `GRPCContiguousBytes` can't
/// refine `SwiftProtobufContiguousBytes` for the same reason. /// refine `SwiftProtobufContiguousBytes` for the same reason.
@usableFromInline @usableFromInline
@available(gRPCSwiftProtobuf 2.0, *)
struct ContiguousBytesAdapter< struct ContiguousBytesAdapter<
Bytes: GRPCContiguousBytes Bytes: GRPCContiguousBytes
>: GRPCContiguousBytes, SwiftProtobufContiguousBytes { >: GRPCContiguousBytes, SwiftProtobufContiguousBytes {

View File

@ -0,0 +1,78 @@
# API stability of generated code
Understand the impact of changes you make to your Protocol Buffers files on the
generated Swift code.
## Overview
The API of the generated code depends on three factors:
- The contents of the source `.proto` file.
- The options you use when generating the code.
- The code generator (the `protoc-gen-grpc-swift-2` plugin for `protoc`).
While this document applies specifically to the gRPC code generated and *not*
code for messages used as inputs and outputs of each method, the concepts still
broadly apply.
Some of the concepts used in this document are described in more detail in
<doc:Understanding-the-generated-code>.
## The source .proto file
The source `.proto` file defines the API of your service. You'll likely provide
it to users so that they can generate clients from it. In order to maintain API
stability for your service and for clients you must adhere to the following
rules:
1. You mustn't change the `package` the service is defined in.
2. You mustn't change or add the `swift_prefix` option.
3. You mustn't remove or change the name of any services in your `.proto` file.
4. You mustn't remove or change the name of any RPCs in your `.proto` file.
5. You mustn't change the message types used by any RPCs in your `.proto` file.
Failure to follow these will result in changes in the generated code which can
result in build failures for users.
Whilst this sounds restrictive you may do the following:
1. You may add a new RPC to an existing service in your `.proto` file.
2. You may add a new service to your `.proto` file (however it is recommended
that you define a single service per `.proto` file).
## The options you use for generating code
Code you generate into your Swift Package becomes part of the API of your
package. You must therefore ensure that downstream users of your package aren't
impacted by the options you use when generating code.
By default code is generated at the `internal` access level and therefore not
part of the public API. You must explicitly opt in to generating code at the
`public` access level. If you do this then you must be aware that changing what
is generated (clients, servers) affects the public API, as does the access level
of the generated code.
If you need to validate whether your API has changed you can use tools like
Swift Package Manager's API breakage diagnostic (`swift package
diagnose-api-breaking-changes`.) In general you should prefer providing users
with the service's `.proto` file so that they can generate clients, or provide a
library which wraps the client to hide the API of the generated code.
## The code generator
The gRPC Swift maintainers may need to evolve the generated code over time. This
will be done in a source-compatible way.
If APIs are no longer suitable then they may be deprecated in favour of new
ones. Within a major version of the package existing API won't be removed
and deprecated APIs will continue to function.
If the generator introduces new ways to generate code which are incompatible
with the previously generated code then they will require explicit opt-in via an
option.
As gRPC Swift is developed the generated code may need to rely on newer
functionality from its runtime counterparts (`GRPCCore` and `GRPCProtobuf`).
This means that you should use the versions of `protoc-gen-grpc-swift-2` and
`protoc-gen-swift` resolved with your package rather than getting them from an
out-of-band (such as `homebrew`).

View File

@ -56,18 +56,18 @@ You must name the file `grpc-swift-proto-generator-config.json` and structure it
"generate": { "generate": {
"clients": true, "clients": true,
"servers": true, "servers": true,
"messages": true, "messages": true
}, },
"generatedSource": { "generatedSource": {
"accessLevelOnImports": false, "accessLevelOnImports": false,
"accessLevel": "internal", "accessLevel": "internal"
}
"protoc": {
"executablePath": "/opt/homebrew/bin/protoc"
"importPaths": [
"../directory_1",
],
}, },
"protoc": {
"executablePath": "/opt/homebrew/bin/protoc",
"importPaths": [
"../directory_1"
]
}
} }
``` ```
@ -87,7 +87,7 @@ The options do not need to be specified and each have default values.
‡ If you don't provide any import paths then the path to the configuration file will be used on a per-source-file basis. ‡ If you don't provide any import paths then the path to the configuration file will be used on a per-source-file basis.
Many of these options map to `protoc-gen-grpc-swift` and `protoc-gen-swift` options. Many of these options map to `protoc-gen-grpc-swift-2` and `protoc-gen-swift` options.
If you require greater flexibility you may specify more than one configuration file. If you require greater flexibility you may specify more than one configuration file.
Configuration files apply to all `.proto` files equal to or below it in the file hierarchy. A configuration file Configuration files apply to all `.proto` files equal to or below it in the file hierarchy. A configuration file
@ -96,73 +96,82 @@ lower in the file hierarchy supersedes one above it.
### Using protoc ### Using protoc
The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides
`protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers compiler, `protoc`. `protoc-gen-grpc-swift-2`, a program which is a plugin for the Protocol Buffers compiler, `protoc`.
To generate gRPC stubs for your `.proto` files directly you must run the `protoc` command with To generate gRPC stubs for your `.proto` files directly you must run the `protoc` command with
the `--grpc-swift_out=<DIRECTORY>` option: the `--grpc-swift-2_out=<DIRECTORY>` option:
```console ```console
protoc --grpc-swift_out=. my-service.proto protoc --grpc-swift-2_out=. my-service.proto
``` ```
> `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use > `protoc-gen-grpc-swift-2` only generates gRPC stubs, it doesn't generate messages. You must use
> `protoc-gen-swift` to generate messages in addition to gRPC Stubs. > `protoc-gen-swift` to generate messages in addition to gRPC Stubs.
The presence of `--grpc-swift_out` tells `protoc` to use the `protoc-gen-grpc-swift` plugin. By The presence of `--grpc-swift-2_out` tells `protoc` to use the `protoc-gen-grpc-swift-2` plugin. By
default it'll look for the plugin in your `PATH`. You can also specify the path to the plugin default it'll look for the plugin in your `PATH`. You can also specify the path to the plugin
explicitly: explicitly:
```console ```console
protoc --plugin=/path/to/protoc-gen-grpc-swift --grpc-swift_out=. my-service.proto protoc --plugin=/path/to/protoc-gen-grpc-swift-2 --grpc-swift-2_out=. my-service.proto
``` ```
You can also specify various option the `protoc-gen-grpc-swift` via `protoc` using You can also specify various option the `protoc-gen-grpc-swift-2` via `protoc` using
the `--grpc-swift_opt` argument: the `--grpc-swift-2_opt` argument:
```console ```console
protoc --grpc-swift_opt=<OPTION_NAME>=<OPTION_VALUE> --grpc-swift_out=. protoc --grpc-swift-2_opt=<OPTION_NAME>=<OPTION_VALUE> --grpc-swift-2_out=.
``` ```
You can specify multiple options by passing the `--grpc-swift_opt` argument multiple times: You can specify multiple options by passing the `--grpc-swift-2_opt` argument multiple times:
```console ```console
protoc \ protoc \
--grpc-swift_opt=<OPTION_NAME1>=<OPTION_VALUE1> \ --grpc-swift-2_opt=<OPTION_NAME1>=<OPTION_VALUE1> \
--grpc-swift_opt=<OPTION_NAME2>=<OPTION_VALUE2> \ --grpc-swift-2_opt=<OPTION_NAME2>=<OPTION_VALUE2> \
--grpc-swift_out=. --grpc-swift-2_out=.
``` ```
#### Generator options #### Generator options
| Name | Possible Values | Default | Description | | Name | Possible Values | Default | Description |
|---------------------------|--------------------------------------------|------------|----------------------------------------------------------| |---------------------------|---------------------------------------------|-----------------|----------------------------------------------------------|
| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | | `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs |
| `Server` | `True`, `False` | `True` | Generate server stubs | | `Server` | `True`, `False` | `True` | Generate server stubs |
| `Client` | `True`, `False` | `True` | Generate client stubs | | `Client` | `True`, `False` | `True` | Generate client stubs |
| `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | | `FileNaming` | `FullPath`, `PathToUnderscores`, `DropPath` | `FullPath` | How generated source files should be named. † |
| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | | `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. ‡ |
| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. | | `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. |
| `GRPCModuleName` | | `GRPCCore` | The name of the `GRPCCore` module. |
| `GRPCProtobufModuleName` | | `GRPCProtobuf` | The name of the `GRPCProtobuf` module. |
| `SwiftProtobufModuleName` | | `SwiftProtobuf` | The name of the `SwiftProtobuf` module. |
| `Availability` | String, in the form `OS Version` | | Platform availability to use in generated code. § |
The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following
output file will be generated: output file will be generated:
- `FullPath`: `foo/bar/baz.grpc.swift`. - `FullPath`: `foo/bar/baz.grpc.swift`.
- `PathToUnderscore`: `foo_bar_baz.grpc.swift` - `PathToUnderscores`: `foo_bar_baz.grpc.swift`
- `DropPath`: `baz.grpc.swift` - `DropPath`: `baz.grpc.swift`
The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings` The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings`
allows you to specify a mapping from `.proto` files to the Swift module they are generated in. This allows you to specify a mapping from `.proto` files to the Swift module they are generated in. This
allows the code generator to add appropriate imports to your generated stubs. This is described in allows the code generator to add appropriate imports to your generated stubs. This is described in
more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md).
§ If unspecified the following availability is used: macOS 15, iOS 18, tvOS 18,
watchOS 11, visionOS 2. The `Availability` option may be specified multiple
times, where each value is a space delimited pair of platform and version, e.g.
`Availability=macOS 15.0`.
#### Building the protoc plugin #### Building the protoc plugin
> The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of > The version of `protoc-gen-grpc-swift-2` you use mustn't be newer than the version of
> the `grpc-swift-protobuf` you're using. > the `grpc-swift-protobuf` you're using.
If your package depends on `grpc-swift-protobuf` then you can get a copy of `protoc-gen-grpc-swift` If your package depends on `grpc-swift-protobuf` then you can get a copy of `protoc-gen-grpc-swift-2`
by building it directly: by building it directly:
```console ```console
swift build --product protoc-gen-grpc-swift swift build --product protoc-gen-grpc-swift-2
``` ```
This command will build the plugin into `.build/debug` directory. You can get the full path using This command will build the plugin into `.build/debug` directory. You can get the full path using

View File

@ -0,0 +1,45 @@
# Public services with private implementations
Learn how to create a `public` gRPC service with private implementation details.
## Overview
It's not uncommon for a library to provide a gRPC service as part of its API.
For example, the gRPC Swift Extras package provides implementations of the gRPC
health and reflection services. Making the implementation of a service `public`
would require its generated gRPC and message types to also be `public`. This is
undesirable as it leaks implementation details into the public API of the
package. This article explains how to keep the generated types private while
making the service available as part of the public API.
## Hiding the implementation
You can hide the implementation details of your service by providing a wrapper
type conforming to `RegistrableRPCService`. This is the protocol used by
`GRPCServer` to register service methods with the server's router. Implementing
`RegistrableRPCService` is straightforward and can delegate to the underlying
service. This is demonstrated in the following code:
```swift
public struct GreeterService: RegistrableRPCService {
private var base: Greeter
public init() {
self.base = Greeter()
}
public func registerMethods<Transport>(
with router: inout RPCRouter<Transport>
) where Transport: ServerTransport {
self.base.registerMethods(with: &router)
}
}
```
In this example `Greeter` implements the underlying service and would conform to
the generated service protocol but would have a non-public access level.
`GreeterService` is a public wrapper type conforming to `RegistrableRPCService`
which implements its only requirement, `registerMethods(with:)`, by calling
through to the underlying implementation. The result is a service which can be
registered with a server where none of the generated types are part of the
public API.

View File

@ -0,0 +1,143 @@
# Understanding the generated code
Understand what code is generated by `protoc-gen-grpc-swift-2` from a `.proto`
file and how to use it.
## Overview
The gRPC Swift Protobuf package provides a plugin to the Protocol Buffers
Compiler (`protoc`) called `protoc-gen-grpc-swift-2`. The plugin is responsible
for generating the gRPC specific code for services defined in a `.proto` file.
### Package namespace
Most `.proto` files contain a `package` directive near the start of the file
describing the namespace it belongs to. Here's an example:
```proto
package foo.bar.v1;
```
The package name "foo.bar.v1" is important as it is used as a prefix for
generated types. The default behaviour is to replace periods with underscores
and to capitalize each word and add a trailing underscore. For this package the
prefix is "Foo\_Bar\_V1\_". If you don't declare a package then the prefix will be
the empty string.
You can override the prefix by setting the `swift_prefix` option:
```proto
option swift_prefix = "FooBarV1";
package foo.bar.v1;
```
The prefix for types in this file would be "FooBarV1" instead of "Foo\_Bar\_V1\_".
### Service namespace
For each service declared in your `.proto` file, gRPC will generate a caseless
`enum` which is a namespace holding the generated protocols and types. The name
of this `enum` is `{PREFIX}{SERVICE}` where `{PREFIX}` is as described in the
previous section and `{SERVICE}` is the name of the service as declared in the
`.proto` file.
As an example the following definition creates a service namespace `enum` called
`Foo_Bar_V1_BazService` (the `{PREFIX}` is "Foo_Bar_V1_" and `{SERVICE}` is
"BazService"):
```proto
package foo.bar.v1;
service BazService {
// ...
}
```
Code generated for each service falls into three categories:
1. Service metadata,
2. Service code, and
3. Client code.
#### Service metadata
gRPC generates metadata for each service including a descriptor identifying the
fully qualified name of the service and information about each method in the
service. You likely won't need to interact directly with this information but
it's available should you need to.
#### Service code
Within a service namespace gRPC generates three service protocols:
1. `StreamingServiceProtocol`,
2. `ServiceProtocol`, and
3. `SimpleServiceProtocol`.
The full name of each protocol includes the service namespace.
> Example:
>
> For the `BazService` in the `foo.bar.v1` package the protocols would be:
>
> - `Foo_Bar_V1_BazService.StreamingServiceProtocol`,
> - `Foo_Bar_V1_BazService.ServiceProtocol`, and
> - `Foo_Bar_V1_BazService.SimpleServiceProtocol`.
Each of these protocols mirror the `service` defined in your `.proto` file with
one requirement per RPC. To implement your service you must implement one of
these protocols.
The protocols form a hierarchy with `StreamingServiceProtocol` at the bottom and
`SimpleServiceProtocol` at the top. `ServiceProtocol` refines
`StreamingServiceProtocol`, and `SimpleServiceProtocol` refines
`ServiceProtocol` (and `StreamingServiceProtocol` in turn).
The `StreamingServiceProtocol` implements each RPC as if it were a bidirectional
streaming RPC. This gives you the most flexibility at the cost of a harder to
implement API. It also puts the responsibility on you to ensure that each RPC
sends and receives the correct number of messages.
The `ServiceProtocol` enforces that the correct number of messages are sent and
received via its API. It also allows you to read request metadata and send both
initial and trailing metadata. The request and response types for these
requirements are in terms of `ServerRequest` and `ServerResponse`.
The `SimpleServiceProtocol` also enforces the correct number of messages are
sent and received via its API. However, it isn't defined in terms of
`ServerRequest` or `ServerResponse` so it doesn't allow you access metadata.
This limitation allows it to have the simplest API and is preferred if you don't
need access to metadata.
| Protocol | Enforces number of messages | Access to metadata
|----------------------------|-----------------------------|-------------------
| `StreamingServiceProtocol` | ✗ | ✓
| `ServiceProtocol` | ✓ | ✓
| `SimpleServiceProtocol` | ✓ | ✗
#### Client code
gRPC generates two types for the client within a service namespace:
1. `ClientProtocol`, and
2. `Client`.
Like the service code, the full name includes the namespace.
> Example:
>
> For the `BazService` in the `foo.bar.v1` package the client types would be:
>
> - `Foo_Bar_V1_BazService.ClientProtocol`, and
> - `Foo_Bar_V1_BazService.Client`.
The `ClientProtocol` defines one requirement for each RPC in terms of
`ClientRequest` and `ClientResponse`. You don't need to implement the protocol
as `Client` provides a concrete implementation.
gRPC also generates extensions on `ClientProtocol` to provide more ergonomic
APIs. These include versions which provide default arguments for various
parameters (like the message serializer and deserializers; call options and
response handler) and versions which don't use `ClientRequest` and
`ClientResponse` directly.

View File

@ -7,7 +7,7 @@ A package integrating Swift Protobuf with gRPC Swift.
This package provides three products: This package provides three products:
- ``GRPCProtobuf``, a module providing runtime serialization and deserialization components for - ``GRPCProtobuf``, a module providing runtime serialization and deserialization components for
[SwiftProtobuf](https://github.com/apple/swift-protobuf). [SwiftProtobuf](https://github.com/apple/swift-protobuf).
- `protoc-gen-grpc-swift`, an executable which is a plugin for `protoc`, the Protocol Buffers - `protoc-gen-grpc-swift-2`, an executable which is a plugin for `protoc`, the Protocol Buffers
compiler. An article describing how to generate gRPC Swift stubs using it is available with the compiler. An article describing how to generate gRPC Swift stubs using it is available with the
`grpc-swift` documentation on the [Swift Package `grpc-swift` documentation on the [Swift Package
Index](https://swiftpackageindex.com/grpc/grpc-swift/documentation). Index](https://swiftpackageindex.com/grpc/grpc-swift/documentation).
@ -21,6 +21,9 @@ This package provides three products:
- <doc:Installing-protoc> - <doc:Installing-protoc>
- <doc:Generating-stubs> - <doc:Generating-stubs>
- <doc:API-stability-of-generated-code>
- <doc:Understanding-the-generated-code>
- <doc:Public-services-with-private-implementations>
### Serialization ### Serialization

View File

@ -61,6 +61,7 @@ extension Google_Protobuf_Any {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails { extension ErrorDetails {
// Note: this type isn't packable into an 'Any' protobuf so doesn't conform // Note: this type isn't packable into an 'Any' protobuf so doesn't conform
// to 'GoogleProtobufAnyPackable' despite holding types which are packable. // to 'GoogleProtobufAnyPackable' despite holding types which are packable.

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails: CustomStringConvertible { extension ErrorDetails: CustomStringConvertible {
public var description: String { public var description: String {
switch self.wrapped { switch self.wrapped {
@ -46,54 +47,63 @@ extension ErrorDetails: CustomStringConvertible {
// Some errors use protobuf messages as their storage so the default description isn't // Some errors use protobuf messages as their storage so the default description isn't
// representative // representative
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ErrorInfo: CustomStringConvertible { extension ErrorDetails.ErrorInfo: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(reason: \"\(self.reason)\", domain: \"\(self.domain)\", metadata: \(self.metadata))" "\(Self.self)(reason: \"\(self.reason)\", domain: \"\(self.domain)\", metadata: \(self.metadata))"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.DebugInfo: CustomStringConvertible { extension ErrorDetails.DebugInfo: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(stack: \(self.stack), detail: \"\(self.detail)\")" "\(Self.self)(stack: \(self.stack), detail: \"\(self.detail)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.QuotaFailure.Violation: CustomStringConvertible { extension ErrorDetails.QuotaFailure.Violation: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(subject: \"\(self.subject)\", violationDescription: \"\(self.violationDescription)\")" "\(Self.self)(subject: \"\(self.subject)\", violationDescription: \"\(self.violationDescription)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.PreconditionFailure.Violation: CustomStringConvertible { extension ErrorDetails.PreconditionFailure.Violation: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(subject: \"\(self.subject)\", type: \"\(self.type)\", violationDescription: \"\(self.violationDescription)\")" "\(Self.self)(subject: \"\(self.subject)\", type: \"\(self.type)\", violationDescription: \"\(self.violationDescription)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.BadRequest.FieldViolation: CustomStringConvertible { extension ErrorDetails.BadRequest.FieldViolation: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(field: \"\(self.field)\", violationDescription: \"\(self.violationDescription)\")" "\(Self.self)(field: \"\(self.field)\", violationDescription: \"\(self.violationDescription)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.RequestInfo: CustomStringConvertible { extension ErrorDetails.RequestInfo: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(requestID: \"\(self.requestID)\", servingData: \"\(self.servingData)\")" "\(Self.self)(requestID: \"\(self.requestID)\", servingData: \"\(self.servingData)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ResourceInfo: CustomStringConvertible { extension ErrorDetails.ResourceInfo: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(name: \"\(self.name)\", owner: \"\(self.owner)\", type: \"\(self.type)\", errorDescription: \"\(self.errorDescription)\")" "\(Self.self)(name: \"\(self.name)\", owner: \"\(self.owner)\", type: \"\(self.type)\", errorDescription: \"\(self.errorDescription)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.Help.Link: CustomStringConvertible { extension ErrorDetails.Help.Link: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(url: \"\(self.url)\", linkDescription: \"\(self.linkDescription)\")" "\(Self.self)(url: \"\(self.url)\", linkDescription: \"\(self.linkDescription)\")"
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.LocalizedMessage: CustomStringConvertible { extension ErrorDetails.LocalizedMessage: CustomStringConvertible {
public var description: String { public var description: String {
"\(Self.self)(locale: \"\(self.locale)\", message: \"\(self.message)\")" "\(Self.self)(locale: \"\(self.locale)\", message: \"\(self.message)\")"

View File

@ -16,6 +16,7 @@
internal import SwiftProtobuf internal import SwiftProtobuf
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails { extension ErrorDetails {
/// Describes the cause of the error with structured details. /// Describes the cause of the error with structured details.
/// ///

View File

@ -24,6 +24,7 @@ public import SwiftProtobuf
/// ///
/// This type also allows you to provide wrap your own error details up as an "Any" /// This type also allows you to provide wrap your own error details up as an "Any"
/// protobuf (`Google_Protobuf_Any`). /// protobuf (`Google_Protobuf_Any`).
@available(gRPCSwiftProtobuf 2.0, *)
public struct ErrorDetails: Sendable, Hashable { public struct ErrorDetails: Sendable, Hashable {
enum Wrapped: Sendable, Hashable { enum Wrapped: Sendable, Hashable {
case errorInfo(ErrorInfo) case errorInfo(ErrorInfo)
@ -198,6 +199,7 @@ public struct ErrorDetails: Sendable, Hashable {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails { extension ErrorDetails {
/// Returns error info if set. /// Returns error info if set.
public var errorInfo: ErrorInfo? { public var errorInfo: ErrorInfo? {

View File

@ -15,7 +15,7 @@
*/ */
public import GRPCCore public import GRPCCore
internal import SwiftProtobuf public import SwiftProtobuf
/// An error containing structured details which can be delivered to the client. /// An error containing structured details which can be delivered to the client.
/// ///
@ -33,10 +33,10 @@ internal import SwiftProtobuf
/// > /// >
/// > The error information is transmitted to clients in the trailing metadata of an RPC. It is /// > The error information is transmitted to clients in the trailing metadata of an RPC. It is
/// > inserted into the metadata keyed by "grpc-status-details-bin". The value of the metadata is /// > inserted into the metadata keyed by "grpc-status-details-bin". The value of the metadata is
/// > the serialized bytes of a "google.protobuf.Any" protocol buffers message. The content of which /// > the serialized bytes of a "google.rpc.Status" protocol buffers message containing the status
/// > is a "google.rpc.Status" protocol buffers message containing the status code, message, and /// > code, message, and details.
/// > details. @available(gRPCSwiftProtobuf 2.0, *)
public struct GoogleRPCStatus: Error { public struct GoogleRPCStatus: Error, Hashable {
/// A code representing the high-level domain of the error. /// A code representing the high-level domain of the error.
public var code: RPCError.Code public var code: RPCError.Code
@ -74,13 +74,33 @@ public struct GoogleRPCStatus: Error {
} }
} }
extension GoogleRPCStatus: GoogleProtobufAnyPackable { @available(gRPCSwiftProtobuf 2.0, *)
// See https://protobuf.dev/programming-guides/proto3/#any extension GoogleRPCStatus {
internal static var typeURL: String { "type.googleapis.com/google.rpc.Status" } /// Creates a new message by decoding the given `SwiftProtobufContiguousBytes` value
/// containing a serialized message in Protocol Buffer binary format.
init?(unpacking any: Google_Protobuf_Any) throws { ///
guard any.isA(Google_Rpc_Status.self) else { return nil } /// - Parameters:
let status = try Google_Rpc_Status(serializedBytes: any.value) /// - bytes: The binary-encoded message data to decode.
/// - extensions: An `ExtensionMap` used to look up and decode any
/// extensions in this message or messages nested within this message's
/// fields.
/// - partial: If `false` (the default), this method will check if the `Message`
/// is initialized after decoding to verify that all required fields are present.
/// If any are missing, this method throws `BinaryDecodingError`.
/// - options: The `BinaryDecodingOptions` to use.
/// - Throws: `BinaryDecodingError` if decoding fails.
public init<Bytes: SwiftProtobufContiguousBytes>(
serializedBytes bytes: Bytes,
extensions: (any ExtensionMap)? = nil,
partial: Bool = false,
options: BinaryDecodingOptions = BinaryDecodingOptions()
) throws {
let status = try Google_Rpc_Status(
serializedBytes: bytes,
extensions: extensions,
partial: partial,
options: options
)
let statusCode = Status.Code(rawValue: Int(status.code)) let statusCode = Status.Code(rawValue: Int(status.code))
self.code = statusCode.flatMap { RPCError.Code($0) } ?? .unknown self.code = statusCode.flatMap { RPCError.Code($0) } ?? .unknown
@ -88,27 +108,40 @@ extension GoogleRPCStatus: GoogleProtobufAnyPackable {
self.details = try status.details.map { try ErrorDetails(unpacking: $0) } self.details = try status.details.map { try ErrorDetails(unpacking: $0) }
} }
func pack() throws -> Google_Protobuf_Any { /// Returns a `SwiftProtobufContiguousBytes` instance containing the Protocol Buffer binary
/// format serialization of the message.
///
/// - Parameters:
/// - partial: If `false` (the default), this method will check
/// `Message.isInitialized` before encoding to verify that all required
/// fields are present. If any are missing, this method throws.
/// `BinaryEncodingError/missingRequiredFields`.
/// - options: The `BinaryEncodingOptions` to use.
/// - Returns: A `SwiftProtobufContiguousBytes` instance containing the binary serialization
/// of the message.
///
/// - Throws: `SwiftProtobufError` or `BinaryEncodingError` if encoding fails.
public func serializedBytes<Bytes: SwiftProtobufContiguousBytes>(
partial: Bool = false,
options: BinaryEncodingOptions = BinaryEncodingOptions()
) throws -> Bytes {
let status = try Google_Rpc_Status.with { let status = try Google_Rpc_Status.with {
$0.code = Int32(self.code.rawValue) $0.code = Int32(self.code.rawValue)
$0.message = self.message $0.message = self.message
$0.details = try self.details.map { try $0.pack() } $0.details = try self.details.map { try $0.pack() }
} }
return try .with { return try status.serializedBytes(partial: partial, options: options)
$0.typeURL = Self.typeURL
$0.value = try status.serializedBytes()
}
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GoogleRPCStatus: RPCErrorConvertible { extension GoogleRPCStatus: RPCErrorConvertible {
public var rpcErrorCode: RPCError.Code { self.code } public var rpcErrorCode: RPCError.Code { self.code }
public var rpcErrorMessage: String { self.message } public var rpcErrorMessage: String { self.message }
public var rpcErrorMetadata: Metadata { public var rpcErrorMetadata: Metadata {
do { do {
let any = try self.pack() let bytes: [UInt8] = try self.serializedBytes()
let bytes: [UInt8] = try any.serializedBytes()
return [Metadata.statusDetailsBinKey: .binary(bytes)] return [Metadata.statusDetailsBinKey: .binary(bytes)]
} catch { } catch {
// Failed to serialize error details. Not a lot can be done here. // Failed to serialize error details. Not a lot can be done here.

View File

@ -17,10 +17,12 @@
public import GRPCCore public import GRPCCore
internal import SwiftProtobuf internal import SwiftProtobuf
@available(gRPCSwiftProtobuf 2.0, *)
extension Metadata { extension Metadata {
static let statusDetailsBinKey = "grpc-status-details-bin" static let statusDetailsBinKey = "grpc-status-details-bin"
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension RPCError { extension RPCError {
/// Unpack a ``GoogleRPCStatus`` error from the error metadata. /// Unpack a ``GoogleRPCStatus`` error from the error metadata.
/// ///
@ -31,8 +33,6 @@ extension RPCError {
public func unpackGoogleRPCStatus() throws -> GoogleRPCStatus? { public func unpackGoogleRPCStatus() throws -> GoogleRPCStatus? {
let values = self.metadata[binaryValues: Metadata.statusDetailsBinKey] let values = self.metadata[binaryValues: Metadata.statusDetailsBinKey]
guard let bytes = values.first(where: { _ in true }) else { return nil } guard let bytes = values.first(where: { _ in true }) else { return nil }
return try GoogleRPCStatus(serializedBytes: bytes)
let any = try Google_Protobuf_Any(serializedBytes: bytes)
return try GoogleRPCStatus(unpacking: any)
} }
} }

View File

@ -32,19 +32,23 @@ internal import struct Foundation.IndexPath
#endif #endif
/// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object.
@available(gRPCSwiftProtobuf 2.0, *)
package struct ProtobufCodeGenParser { package struct ProtobufCodeGenParser {
let extraModuleImports: [String] let extraModuleImports: [String]
let protoToModuleMappings: ProtoFileToModuleMappings let protoToModuleMappings: ProtoFileToModuleMappings
let accessLevel: CodeGenerator.Config.AccessLevel let accessLevel: CodeGenerator.Config.AccessLevel
let moduleNames: ProtobufCodeGenerator.Config.ModuleNames
package init( package init(
protoFileModuleMappings: ProtoFileToModuleMappings, protoFileModuleMappings: ProtoFileToModuleMappings,
extraModuleImports: [String], extraModuleImports: [String],
accessLevel: CodeGenerator.Config.AccessLevel accessLevel: CodeGenerator.Config.AccessLevel,
moduleNames: ProtobufCodeGenerator.Config.ModuleNames
) { ) {
self.extraModuleImports = extraModuleImports self.extraModuleImports = extraModuleImports
self.protoToModuleMappings = protoFileModuleMappings self.protoToModuleMappings = protoFileModuleMappings
self.accessLevel = accessLevel self.accessLevel = accessLevel
self.moduleNames = moduleNames
} }
package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest { package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest {
@ -62,6 +66,7 @@ package struct ProtobufCodeGenParser {
let leadingTrivia = """ let leadingTrivia = """
// DO NOT EDIT. // DO NOT EDIT.
// swift-format-ignore-file // swift-format-ignore-file
// swiftlint:disable all
// //
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: \(descriptor.name) // Source: \(descriptor.name)
@ -86,15 +91,16 @@ package struct ProtobufCodeGenParser {
dependencies: self.codeDependencies(file: descriptor), dependencies: self.codeDependencies(file: descriptor),
services: services, services: services,
makeSerializerCodeSnippet: { messageType in makeSerializerCodeSnippet: { messageType in
"GRPCProtobuf.ProtobufSerializer<\(messageType)>()" "\(self.moduleNames.grpcProtobuf).ProtobufSerializer<\(messageType)>()"
}, },
makeDeserializerCodeSnippet: { messageType in makeDeserializerCodeSnippet: { messageType in
"GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" "\(self.moduleNames.grpcProtobuf).ProtobufDeserializer<\(messageType)>()"
} }
) )
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ProtobufCodeGenParser { extension ProtobufCodeGenParser {
fileprivate func codeDependencies(file: FileDescriptor) -> [Dependency] { fileprivate func codeDependencies(file: FileDescriptor) -> [Dependency] {
guard file.services.count > 0 else { guard file.services.count > 0 else {
@ -102,7 +108,7 @@ extension ProtobufCodeGenParser {
} }
var codeDependencies: [Dependency] = [ var codeDependencies: [Dependency] = [
Dependency(module: "GRPCProtobuf", accessLevel: .internal) Dependency(module: self.moduleNames.grpcProtobuf, accessLevel: .internal)
] ]
// If there's a dependency on a bundled proto then add the SwiftProtobuf import. // If there's a dependency on a bundled proto then add the SwiftProtobuf import.
// //
@ -113,7 +119,11 @@ extension ProtobufCodeGenParser {
} }
if dependsOnBundledProto { if dependsOnBundledProto {
codeDependencies.append(Dependency(module: "SwiftProtobuf", accessLevel: self.accessLevel)) let dependency = Dependency(
module: self.moduleNames.swiftProtobuf,
accessLevel: self.accessLevel
)
codeDependencies.append(dependency)
} }
// Adding as dependencies the modules containing generated code or types for // Adding as dependencies the modules containing generated code or types for
@ -133,6 +143,7 @@ extension ProtobufCodeGenParser {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCodeGen.ServiceDescriptor { extension GRPCCodeGen.ServiceDescriptor {
fileprivate init( fileprivate init(
descriptor: SwiftProtobufPluginLibrary.ServiceDescriptor, descriptor: SwiftProtobufPluginLibrary.ServiceDescriptor,
@ -160,6 +171,7 @@ extension GRPCCodeGen.ServiceDescriptor {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCodeGen.MethodDescriptor { extension GRPCCodeGen.MethodDescriptor {
fileprivate init( fileprivate init(
descriptor: SwiftProtobufPluginLibrary.MethodDescriptor, descriptor: SwiftProtobufPluginLibrary.MethodDescriptor,

View File

@ -17,11 +17,12 @@
package import GRPCCodeGen package import GRPCCodeGen
package import SwiftProtobufPluginLibrary package import SwiftProtobufPluginLibrary
@available(gRPCSwiftProtobuf 2.0, *)
package struct ProtobufCodeGenerator { package struct ProtobufCodeGenerator {
internal var config: GRPCCodeGen.CodeGenerator.Config internal var config: ProtobufCodeGenerator.Config
package init( package init(
config: GRPCCodeGen.CodeGenerator.Config config: ProtobufCodeGenerator.Config
) { ) {
self.config = config self.config = config
} }
@ -29,17 +30,76 @@ package struct ProtobufCodeGenerator {
package func generateCode( package func generateCode(
fileDescriptor: FileDescriptor, fileDescriptor: FileDescriptor,
protoFileModuleMappings: ProtoFileToModuleMappings, protoFileModuleMappings: ProtoFileToModuleMappings,
extraModuleImports: [String] extraModuleImports: [String],
availabilityOverrides: [(os: String, version: String)] = []
) throws -> String { ) throws -> String {
let parser = ProtobufCodeGenParser( let parser = ProtobufCodeGenParser(
protoFileModuleMappings: protoFileModuleMappings, protoFileModuleMappings: protoFileModuleMappings,
extraModuleImports: extraModuleImports, extraModuleImports: extraModuleImports,
accessLevel: self.config.accessLevel accessLevel: self.config.accessLevel,
moduleNames: self.config.moduleNames
) )
let codeGenerator = GRPCCodeGen.CodeGenerator(config: self.config)
var codeGeneratorConfig = GRPCCodeGen.CodeGenerator.Config(
accessLevel: self.config.accessLevel,
accessLevelOnImports: self.config.accessLevelOnImports,
client: self.config.generateClient,
server: self.config.generateServer,
indentation: self.config.indentation
)
codeGeneratorConfig.grpcCoreModuleName = self.config.moduleNames.grpcCore
if availabilityOverrides.isEmpty {
codeGeneratorConfig.availability = .default
} else {
codeGeneratorConfig.availability = .custom(
availabilityOverrides.map { (os, version) in
.init(os: os, version: version)
}
)
}
let codeGenerator = GRPCCodeGen.CodeGenerator(config: codeGeneratorConfig)
let codeGenerationRequest = try parser.parse(descriptor: fileDescriptor) let codeGenerationRequest = try parser.parse(descriptor: fileDescriptor)
let sourceFile = try codeGenerator.generate(codeGenerationRequest) let sourceFile = try codeGenerator.generate(codeGenerationRequest)
return sourceFile.contents return sourceFile.contents
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ProtobufCodeGenerator {
package struct Config {
package var accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel
package var accessLevelOnImports: Bool
package var generateClient: Bool
package var generateServer: Bool
package var indentation: Int
package var moduleNames: ModuleNames
package struct ModuleNames {
package var grpcCore: String
package var grpcProtobuf: String
package var swiftProtobuf: String
package static let defaults = Self(
grpcCore: "GRPCCore",
grpcProtobuf: "GRPCProtobuf",
swiftProtobuf: "SwiftProtobuf"
)
}
package static var defaults: Self {
Self(
accessLevel: .internal,
accessLevelOnImports: false,
generateClient: true,
generateServer: true,
indentation: 4,
moduleNames: .defaults
)
}
}
}

View File

@ -26,6 +26,7 @@ import Foundation
#endif #endif
@main @main
@available(gRPCSwiftProtobuf 2.0, *)
final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator { final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
var version: String? { var version: String? {
Version.versionString Version.versionString
@ -55,37 +56,10 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
let options = try GeneratorOptions(parameter: parameter) let options = try GeneratorOptions(parameter: parameter)
for descriptor in fileDescriptors { for descriptor in fileDescriptors {
if options.generateReflectionData {
try self.generateReflectionData(
descriptor,
options: options,
outputs: outputs
)
}
try self.generateV2Stubs(descriptor, options: options, outputs: outputs) try self.generateV2Stubs(descriptor, options: options, outputs: outputs)
} }
} }
private func generateReflectionData(
_ descriptor: FileDescriptor,
options: GeneratorOptions,
outputs: any GeneratorOutputs
) throws {
let fileName = self.uniqueOutputFileName(
fileDescriptor: descriptor,
fileNamingOption: options.fileNaming,
extension: "reflection"
)
var options = ExtractProtoOptions()
options.includeSourceCodeInfo = true
let proto = descriptor.extractProto(options: options)
let serializedProto = try proto.serializedData()
let reflectionData = serializedProto.base64EncodedString()
try outputs.add(fileName: fileName, contents: reflectionData)
}
private func generateV2Stubs( private func generateV2Stubs(
_ descriptor: FileDescriptor, _ descriptor: FileDescriptor,
options: GeneratorOptions, options: GeneratorOptions,
@ -96,18 +70,19 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
fileNamingOption: options.fileNaming fileNamingOption: options.fileNaming
) )
let config = CodeGenerator.Config(options: options) let fileGenerator = ProtobufCodeGenerator(config: options.config)
let fileGenerator = ProtobufCodeGenerator(config: config)
let contents = try fileGenerator.generateCode( let contents = try fileGenerator.generateCode(
fileDescriptor: descriptor, fileDescriptor: descriptor,
protoFileModuleMappings: options.protoToModuleMappings, protoFileModuleMappings: options.protoToModuleMappings,
extraModuleImports: options.extraModuleImports extraModuleImports: options.extraModuleImports,
availabilityOverrides: options.availabilityOverrides
) )
try outputs.add(fileName: fileName, contents: contents) try outputs.add(fileName: fileName, contents: contents)
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GenerateGRPC { extension GenerateGRPC {
private func uniqueOutputFileName( private func uniqueOutputFileName(
fileDescriptor: FileDescriptor, fileDescriptor: FileDescriptor,
@ -181,24 +156,3 @@ private func splitPath(pathname: String) -> (dir: String, base: String, suffix:
} }
return (dir: dir, base: base, suffix: suffix) return (dir: dir, base: base, suffix: suffix)
} }
extension GRPCCodeGen.CodeGenerator.Config {
init(options: GeneratorOptions) {
let accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel
switch options.visibility {
case .internal:
accessLevel = .internal
case .package:
accessLevel = .package
case .public:
accessLevel = .public
}
self.init(
accessLevel: accessLevel,
accessLevelOnImports: options.useAccessLevelOnImports,
client: options.generateClient,
server: options.generateServer
)
}
}

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import GRPCCodeGen
import GRPCProtobufCodeGen
import SwiftProtobufPluginLibrary import SwiftProtobufPluginLibrary
enum GenerationError: Error, CustomStringConvertible { enum GenerationError: Error, CustomStringConvertible {
@ -23,6 +25,8 @@ enum GenerationError: Error, CustomStringConvertible {
case invalidParameterValue(name: String, value: String) case invalidParameterValue(name: String, value: String)
/// Raised to wrap another error but provide a context message. /// Raised to wrap another error but provide a context message.
case wrappedError(message: String, error: any Error) case wrappedError(message: String, error: any Error)
/// The parameter isn't supported.
case unsupportedParameter(name: String, message: String)
var description: String { var description: String {
switch self { switch self {
@ -32,6 +36,8 @@ enum GenerationError: Error, CustomStringConvertible {
return "Unknown value for generation parameter '\(name)': '\(value)'" return "Unknown value for generation parameter '\(name)': '\(value)'"
case let .wrappedError(message, error): case let .wrappedError(message, error):
return "\(message): \(error)" return "\(message): \(error)"
case let .unsupportedParameter(name, message):
return "Unsupported parameter '\(name)': \(message)"
} }
} }
} }
@ -42,36 +48,14 @@ enum FileNaming: String {
case dropPath = "DropPath" case dropPath = "DropPath"
} }
@available(gRPCSwiftProtobuf 2.0, *)
struct GeneratorOptions { struct GeneratorOptions {
enum Visibility: String {
case `internal` = "Internal"
case `public` = "Public"
case `package` = "Package"
var sourceSnippet: String {
switch self {
case .internal:
return "internal"
case .public:
return "public"
case .package:
return "package"
}
}
}
private(set) var visibility = Visibility.internal
private(set) var generateServer = true
private(set) var generateClient = true
private(set) var protoToModuleMappings = ProtoFileToModuleMappings() private(set) var protoToModuleMappings = ProtoFileToModuleMappings()
private(set) var fileNaming = FileNaming.fullPath private(set) var fileNaming = FileNaming.fullPath
private(set) var extraModuleImports: [String] = [] private(set) var extraModuleImports: [String] = []
private(set) var gRPCModuleName = "GRPC" private(set) var availabilityOverrides: [(os: String, version: String)] = []
private(set) var swiftProtobufModuleName = "SwiftProtobuf"
private(set) var generateReflectionData = false private(set) var config: ProtobufCodeGenerator.Config = .defaults
private(set) var useAccessLevelOnImports = false
init(parameter: any CodeGeneratorParameter) throws { init(parameter: any CodeGeneratorParameter) throws {
try self.init(pairs: parameter.parsedPairs) try self.init(pairs: parameter.parsedPairs)
@ -81,22 +65,22 @@ struct GeneratorOptions {
for pair in pairs { for pair in pairs {
switch pair.key { switch pair.key {
case "Visibility": case "Visibility":
if let value = Visibility(rawValue: pair.value) { if let value = GRPCCodeGen.CodeGenerator.Config.AccessLevel(protocOption: pair.value) {
self.visibility = value self.config.accessLevel = value
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
case "Server": case "Server":
if let value = Bool(pair.value.lowercased()) { if let value = Bool(pair.value.lowercased()) {
self.generateServer = value self.config.generateServer = value
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
case "Client": case "Client":
if let value = Bool(pair.value.lowercased()) { if let value = Bool(pair.value.lowercased()) {
self.generateClient = value self.config.generateClient = value
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
@ -129,28 +113,49 @@ struct GeneratorOptions {
case "GRPCModuleName": case "GRPCModuleName":
if !pair.value.isEmpty { if !pair.value.isEmpty {
self.gRPCModuleName = pair.value self.config.moduleNames.grpcCore = pair.value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "GRPCProtobufModuleName":
if !pair.value.isEmpty {
self.config.moduleNames.grpcProtobuf = pair.value
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
case "SwiftProtobufModuleName": case "SwiftProtobufModuleName":
if !pair.value.isEmpty { if !pair.value.isEmpty {
self.swiftProtobufModuleName = pair.value self.config.moduleNames.swiftProtobuf = pair.value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "Availability":
if !pair.value.isEmpty {
let parts = pair.value.split(separator: " ", maxSplits: 1)
if parts.count == 2 {
self.availabilityOverrides.append((os: String(parts[0]), version: String(parts[1])))
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
case "ReflectionData": case "ReflectionData":
if let value = Bool(pair.value.lowercased()) { throw GenerationError.unsupportedParameter(
self.generateReflectionData = value name: pair.key,
} else { message: """
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) The reflection service uses descriptor sets. Refer to the protoc docs and the \
} '--descriptor_set_out' option for more information.
"""
)
case "UseAccessLevelOnImports": case "UseAccessLevelOnImports":
if let value = Bool(pair.value.lowercased()) { if let value = Bool(pair.value.lowercased()) {
self.useAccessLevelOnImports = value self.config.accessLevelOnImports = value
} else { } else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
} }
@ -187,6 +192,7 @@ struct GeneratorOptions {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension String.SubSequence { extension String.SubSequence {
func trimmingWhitespaceAndNewlines() -> String { func trimmingWhitespaceAndNewlines() -> String {
let trimmedSuffix = self.drop(while: { $0.isNewline || $0.isWhitespace }) let trimmedSuffix = self.drop(while: { $0.isNewline || $0.isWhitespace })
@ -194,3 +200,19 @@ extension String.SubSequence {
return String(trimmed) return String(trimmed)
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCodeGen.CodeGenerator.Config.AccessLevel {
fileprivate init?(protocOption value: String) {
switch value {
case "Internal":
self = .internal
case "Public":
self = .public
case "Package":
self = .package
default:
return nil
}
}
}

View File

@ -14,26 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
#if canImport(CGRPCProtobuf)
private import CGRPCProtobuf
#endif
internal enum Version { internal enum Version {
/// The major version.
internal static let major = 1
/// The minor version.
internal static let minor = 0
/// The patch version.
internal static let patch = 0
/// Any additional label.
internal static let label = "development"
/// The version string. /// The version string.
internal static var versionString: String { internal static var versionString: String {
let version = "\(Self.major).\(Self.minor).\(Self.patch)" #if canImport(CGRPCProtobuf)
if Self.label.isEmpty { String(cString: cgrprc_grpc_swift_protobuf_version())
return version #else
} else { "unknown"
return version + "-" + Self.label #endif
}
} }
} }

View File

@ -27,25 +27,29 @@ struct ProtobufCodeGenParserTests {
static let descriptorSetName = "test-service" static let descriptorSetName = "test-service"
static let fileDescriptorName = "test-service" static let fileDescriptorName = "test-service"
let codeGen: CodeGenerationRequest @available(gRPCSwiftProtobuf 2.0, *)
var codeGen: CodeGenerationRequest {
init() throws { get throws {
let descriptor = try #require(try Self.fileDescriptor) let descriptor = try Self.fileDescriptor
self.codeGen = try parseDescriptor(descriptor) return try parseDescriptor(descriptor)
}
} }
@Test("Filename") @Test("Filename")
func fileName() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.codeGen.fileName == "test-service.proto") func fileName() throws {
#expect(try self.codeGen.fileName == "test-service.proto")
} }
@Test("Leading trivia") @Test("Leading trivia")
func leadingTrivia() { @available(gRPCSwiftProtobuf 2.0, *)
func leadingTrivia() throws {
let expected = """ let expected = """
/// Leading trivia. /// Leading trivia.
// DO NOT EDIT. // DO NOT EDIT.
// swift-format-ignore-file // swift-format-ignore-file
// swiftlint:disable all
// //
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: test-service.proto // Source: test-service.proto
@ -55,94 +59,112 @@ struct ProtobufCodeGenParserTests {
""" """
#expect(self.codeGen.leadingTrivia == expected) #expect(try self.codeGen.leadingTrivia == expected)
} }
@Test("Dependencies") @Test("Dependencies")
func dependencies() { @available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
let expected: [GRPCCodeGen.Dependency] = [ let expected: [GRPCCodeGen.Dependency] = [
.init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import .init(module: "GRPCProtobuf", accessLevel: .internal), // Always an internal import
.init(module: "SwiftProtobuf", accessLevel: .internal),
] ]
#expect(self.codeGen.dependencies == expected) #expect(try self.codeGen.dependencies == expected)
} }
@Suite("Service") @Suite("Service")
struct Service { struct Service {
let service: GRPCCodeGen.ServiceDescriptor @available(gRPCSwiftProtobuf 2.0, *)
var service: GRPCCodeGen.ServiceDescriptor {
init() throws { get throws {
let request = try parseDescriptor(try #require(try TestService.fileDescriptor)) let request = try parseDescriptor(try TestService.fileDescriptor)
try #require(request.services.count == 1) try #require(request.services.count == 1)
self.service = try #require(request.services.first) return try #require(request.services.first)
}
} }
@Test("Name") @Test("Name")
func name() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.service.name.identifyingName == "test.TestService") func name() throws {
#expect(try self.service.name.identifyingName == "test.TestService")
} }
@Suite("Methods") @Suite("Methods")
struct Methods { struct Methods {
let unary: GRPCCodeGen.MethodDescriptor @available(gRPCSwiftProtobuf 2.0, *)
let clientStreaming: GRPCCodeGen.MethodDescriptor var service: GRPCCodeGen.ServiceDescriptor {
let serverStreaming: GRPCCodeGen.MethodDescriptor get throws {
let bidiStreaming: GRPCCodeGen.MethodDescriptor let request = try parseDescriptor(try TestService.fileDescriptor)
try #require(request.services.count == 1)
return try #require(request.services.first)
}
}
init() throws { @available(gRPCSwiftProtobuf 2.0, *)
let request = try parseDescriptor(try #require(try TestService.fileDescriptor)) var unary: GRPCCodeGen.MethodDescriptor {
#expect(request.services.count == 1) get throws { try self.service.methods[0] }
let service = try #require(request.services.first) }
@available(gRPCSwiftProtobuf 2.0, *)
self.unary = service.methods[0] var clientStreaming: GRPCCodeGen.MethodDescriptor {
self.clientStreaming = service.methods[1] get throws { try self.service.methods[1] }
self.serverStreaming = service.methods[2] }
self.bidiStreaming = service.methods[3] @available(gRPCSwiftProtobuf 2.0, *)
var serverStreaming: GRPCCodeGen.MethodDescriptor {
get throws { try self.service.methods[2] }
}
@available(gRPCSwiftProtobuf 2.0, *)
var bidiStreaming: GRPCCodeGen.MethodDescriptor {
get throws { try self.service.methods[3] }
} }
@Test("Documentation") @Test("Documentation")
func documentation() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.unary.documentation == "/// Unary docs.\n") func documentation() throws {
#expect(self.clientStreaming.documentation == "/// Client streaming docs.\n") #expect(try self.unary.documentation == "/// Unary docs.\n")
#expect(self.serverStreaming.documentation == "/// Server streaming docs.\n") #expect(try self.clientStreaming.documentation == "/// Client streaming docs.\n")
#expect(self.bidiStreaming.documentation == "/// Bidirectional streaming docs.\n") #expect(try self.serverStreaming.documentation == "/// Server streaming docs.\n")
#expect(try self.bidiStreaming.documentation == "/// Bidirectional streaming docs.\n")
} }
@Test("Name") @Test("Name")
func name() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.unary.name.identifyingName == "Unary") func name() throws {
#expect(self.clientStreaming.name.identifyingName == "ClientStreaming") try #expect(self.unary.name.identifyingName == "Unary")
#expect(self.serverStreaming.name.identifyingName == "ServerStreaming") try #expect(self.clientStreaming.name.identifyingName == "ClientStreaming")
#expect(self.bidiStreaming.name.identifyingName == "BidirectionalStreaming") try #expect(self.serverStreaming.name.identifyingName == "ServerStreaming")
try #expect(self.bidiStreaming.name.identifyingName == "BidirectionalStreaming")
} }
@Test("Input") @Test("Input")
func input() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.unary.inputType == "Test_TestInput") func input() throws {
#expect(!self.unary.isInputStreaming) #expect(try self.unary.inputType == "Test_TestInput")
#expect(try !self.unary.isInputStreaming)
#expect(self.clientStreaming.inputType == "Test_TestInput") #expect(try self.clientStreaming.inputType == "Test_TestInput")
#expect(self.clientStreaming.isInputStreaming) #expect(try self.clientStreaming.isInputStreaming)
#expect(self.serverStreaming.inputType == "Test_TestInput") #expect(try self.serverStreaming.inputType == "Test_TestInput")
#expect(!self.serverStreaming.isInputStreaming) #expect(try !self.serverStreaming.isInputStreaming)
#expect(self.bidiStreaming.inputType == "Test_TestInput") #expect(try self.bidiStreaming.inputType == "Test_TestInput")
#expect(self.bidiStreaming.isInputStreaming) #expect(try self.bidiStreaming.isInputStreaming)
} }
@Test("Output") @Test("Output")
func output() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.unary.outputType == "Test_TestOutput") func output() throws {
#expect(!self.unary.isOutputStreaming) #expect(try self.unary.outputType == "Test_TestOutput")
#expect(try !self.unary.isOutputStreaming)
#expect(self.clientStreaming.outputType == "Test_TestOutput") #expect(try self.clientStreaming.outputType == "Test_TestOutput")
#expect(!self.clientStreaming.isOutputStreaming) #expect(try !self.clientStreaming.isOutputStreaming)
#expect(self.serverStreaming.outputType == "Test_TestOutput") #expect(try self.serverStreaming.outputType == "Test_TestOutput")
#expect(self.serverStreaming.isOutputStreaming) #expect(try self.serverStreaming.isOutputStreaming)
#expect(self.bidiStreaming.outputType == "Test_TestOutput") #expect(try self.bidiStreaming.outputType == "Test_TestOutput")
#expect(self.bidiStreaming.isOutputStreaming) #expect(try self.bidiStreaming.isOutputStreaming)
} }
} }
} }
@ -153,51 +175,58 @@ struct ProtobufCodeGenParserTests {
static let descriptorSetName = "foo-service" static let descriptorSetName = "foo-service"
static let fileDescriptorName = "foo-service" static let fileDescriptorName = "foo-service"
let codeGen: CodeGenerationRequest @available(gRPCSwiftProtobuf 2.0, *)
var codeGen: CodeGenerationRequest {
init() throws { get throws {
let descriptor = try #require(try Self.fileDescriptor) let descriptor = try Self.fileDescriptor
self.codeGen = try parseDescriptor(descriptor) return try parseDescriptor(descriptor)
}
} }
@Test("Name") @Test("Name")
func name() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.codeGen.fileName == "foo-service.proto") func name() throws {
#expect(try self.codeGen.fileName == "foo-service.proto")
} }
@Test("Dependencies") @Test("Dependencies")
func dependencies() { @available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
let expected: [GRPCCodeGen.Dependency] = [ let expected: [GRPCCodeGen.Dependency] = [
.init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import .init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import
] ]
#expect(self.codeGen.dependencies == expected) #expect(try self.codeGen.dependencies == expected)
} }
@Test("Service1") @Test("Service1")
@available(gRPCSwiftProtobuf 2.0, *)
func service1() throws { func service1() throws {
let service = self.codeGen.services[0] let service = try self.codeGen.services[0]
#expect(service.name.identifyingName == "foo.FooService1") #expect(service.name.identifyingName == "foo.FooService1")
#expect(service.methods.count == 1) #expect(service.methods.count == 1)
} }
@Test("Service1.Method") @Test("Service1.Method")
@available(gRPCSwiftProtobuf 2.0, *)
func service1Method() throws { func service1Method() throws {
let method = self.codeGen.services[0].methods[0] let method = try self.codeGen.services[0].methods[0]
#expect(method.name.identifyingName == "Foo") #expect(method.name.identifyingName == "Foo")
#expect(method.inputType == "Foo_FooInput") #expect(method.inputType == "Foo_FooInput")
#expect(method.outputType == "Foo_FooOutput") #expect(method.outputType == "Foo_FooOutput")
} }
@Test("Service2") @Test("Service2")
@available(gRPCSwiftProtobuf 2.0, *)
func service2() throws { func service2() throws {
let service = self.codeGen.services[1] let service = try self.codeGen.services[1]
#expect(service.name.identifyingName == "foo.FooService2") #expect(service.name.identifyingName == "foo.FooService2")
#expect(service.methods.count == 1) #expect(service.methods.count == 1)
} }
@Test("Service2.Method") @Test("Service2.Method")
@available(gRPCSwiftProtobuf 2.0, *)
func service2Method() throws { func service2Method() throws {
let method = self.codeGen.services[1].methods[0] let method = try self.codeGen.services[1].methods[0]
#expect(method.name.identifyingName == "Foo") #expect(method.name.identifyingName == "Foo")
#expect(method.inputType == "Foo_FooInput") #expect(method.inputType == "Foo_FooInput")
#expect(method.outputType == "Foo_FooOutput") #expect(method.outputType == "Foo_FooOutput")
@ -209,18 +238,14 @@ struct ProtobufCodeGenParserTests {
static var descriptorSetName: String { "bar-service" } static var descriptorSetName: String { "bar-service" }
static var fileDescriptorName: String { "bar-service" } static var fileDescriptorName: String { "bar-service" }
let codeGen: CodeGenerationRequest
let service: GRPCCodeGen.ServiceDescriptor
init() throws {
let descriptor = try #require(try Self.fileDescriptor)
self.codeGen = try parseDescriptor(descriptor)
self.service = try #require(self.codeGen.services.first)
}
@Test("Service name") @Test("Service name")
func serviceName() { @available(gRPCSwiftProtobuf 2.0, *)
#expect(self.service.name.identifyingName == "BarService") func serviceName() throws {
let descriptor = try Self.fileDescriptor
let codeGen = try parseDescriptor(descriptor)
let service = try #require(codeGen.services.first)
#expect(service.name.identifyingName == "BarService")
} }
} }
@ -229,20 +254,17 @@ struct ProtobufCodeGenParserTests {
static let descriptorSetName = "wkt-service" static let descriptorSetName = "wkt-service"
static let fileDescriptorName = "wkt-service" static let fileDescriptorName = "wkt-service"
let codeGen: CodeGenerationRequest
init() throws {
let descriptor = try #require(try Self.fileDescriptor)
self.codeGen = try parseDescriptor(descriptor)
}
@Test("Dependencies") @Test("Dependencies")
func dependencies() { @available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
let descriptor = try Self.fileDescriptor
let codeGen = try parseDescriptor(descriptor)
let expected: [Dependency] = [ let expected: [Dependency] = [
Dependency(module: "GRPCProtobuf", accessLevel: .internal), Dependency(module: "GRPCProtobuf", accessLevel: .internal),
Dependency(module: "SwiftProtobuf", accessLevel: .internal), Dependency(module: "SwiftProtobuf", accessLevel: .internal),
] ]
#expect(self.codeGen.dependencies == expected) #expect(codeGen.dependencies == expected)
} }
} }
} }

View File

@ -25,17 +25,43 @@ struct ProtobufCodeGeneratorTests {
static let descriptorSetName = "test-service" static let descriptorSetName = "test-service"
static let fileDescriptorName = "test-service" static let fileDescriptorName = "test-service"
@Test("Generate", arguments: [CodeGenerator.Config.AccessLevel.internal]) enum Availability {
func generate(accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel) throws { case `default`
let generator = ProtobufCodeGenerator( case fooOS
config: CodeGenerator.Config(
accessLevel: accessLevel, var override: [(String, String)] {
accessLevelOnImports: false, switch self {
client: true, case .default:
server: true, return []
indentation: 2 case .fooOS:
) return [("fooOS", "42.0")]
) }
}
var expected: String {
switch self {
case .default:
return "macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"
case .fooOS:
return "fooOS 42.0"
}
}
}
@available(gRPCSwiftProtobuf 2.0, *)
@Test(
"Generate",
arguments: [CodeGenerator.Config.AccessLevel.internal],
[Availability.default, Availability.fooOS]
)
func generate(
accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel,
availability: Availability
) throws {
var config = ProtobufCodeGenerator.Config.defaults
config.accessLevel = accessLevel
config.indentation = 2
let generator = ProtobufCodeGenerator(config: config)
let access: String let access: String
switch accessLevel { switch accessLevel {
@ -49,10 +75,13 @@ struct ProtobufCodeGeneratorTests {
fatalError() fatalError()
} }
let expectedAvailability = availability.expected
let generated = try generator.generateCode( let generated = try generator.generateCode(
fileDescriptor: Self.fileDescriptor, fileDescriptor: Self.fileDescriptor,
protoFileModuleMappings: ProtoFileToModuleMappings(), protoFileModuleMappings: ProtoFileToModuleMappings(),
extraModuleImports: [] extraModuleImports: [],
availabilityOverrides: availability.override
) )
let expected = """ let expected = """
@ -60,6 +89,7 @@ struct ProtobufCodeGeneratorTests {
// DO NOT EDIT. // DO NOT EDIT.
// swift-format-ignore-file // swift-format-ignore-file
// swiftlint:disable all
// //
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: test-service.proto // Source: test-service.proto
@ -69,10 +99,12 @@ struct ProtobufCodeGeneratorTests {
import GRPCCore import GRPCCore
import GRPCProtobuf import GRPCProtobuf
import SwiftProtobuf
// MARK: - test.TestService // MARK: - test.TestService
/// Namespace containing generated types for the "test.TestService" service. /// Namespace containing generated types for the "test.TestService" service.
@available(\(expectedAvailability), *)
\(access) enum Test_TestService { \(access) enum Test_TestService {
/// Service descriptor for the "test.TestService" service. /// Service descriptor for the "test.TestService" service.
\(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService")
@ -136,6 +168,7 @@ struct ProtobufCodeGeneratorTests {
} }
} }
@available(\(expectedAvailability), *)
extension GRPCCore.ServiceDescriptor { extension GRPCCore.ServiceDescriptor {
/// Service descriptor for the "test.TestService" service. /// Service descriptor for the "test.TestService" service.
\(access) static let test_TestService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") \(access) static let test_TestService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService")
@ -143,6 +176,7 @@ struct ProtobufCodeGeneratorTests {
// MARK: test.TestService (server) // MARK: test.TestService (server)
@available(\(expectedAvailability), *)
extension Test_TestService { extension Test_TestService {
/// Streaming variant of the service protocol for the "test.TestService" service. /// Streaming variant of the service protocol for the "test.TestService" service.
/// ///
@ -404,6 +438,7 @@ struct ProtobufCodeGeneratorTests {
} }
// Default implementation of 'registerMethods(with:)'. // Default implementation of 'registerMethods(with:)'.
@available(\(expectedAvailability), *)
extension Test_TestService.StreamingServiceProtocol { extension Test_TestService.StreamingServiceProtocol {
\(access) func registerMethods<Transport>(with router: inout GRPCCore.RPCRouter<Transport>) where Transport: GRPCCore.ServerTransport { \(access) func registerMethods<Transport>(with router: inout GRPCCore.RPCRouter<Transport>) where Transport: GRPCCore.ServerTransport {
router.registerHandler( router.registerHandler(
@ -454,6 +489,7 @@ struct ProtobufCodeGeneratorTests {
} }
// Default implementation of streaming methods from 'StreamingServiceProtocol'. // Default implementation of streaming methods from 'StreamingServiceProtocol'.
@available(\(expectedAvailability), *)
extension Test_TestService.ServiceProtocol { extension Test_TestService.ServiceProtocol {
\(access) func unary( \(access) func unary(
request: GRPCCore.StreamingServerRequest<Test_TestInput>, request: GRPCCore.StreamingServerRequest<Test_TestInput>,
@ -490,6 +526,7 @@ struct ProtobufCodeGeneratorTests {
} }
// Default implementation of methods from 'ServiceProtocol'. // Default implementation of methods from 'ServiceProtocol'.
@available(\(expectedAvailability), *)
extension Test_TestService.SimpleServiceProtocol { extension Test_TestService.SimpleServiceProtocol {
\(access) func unary( \(access) func unary(
request: GRPCCore.ServerRequest<Test_TestInput>, request: GRPCCore.ServerRequest<Test_TestInput>,
@ -554,6 +591,7 @@ struct ProtobufCodeGeneratorTests {
// MARK: test.TestService (client) // MARK: test.TestService (client)
@available(\(expectedAvailability), *)
extension Test_TestService { extension Test_TestService {
/// Generated client protocol for the "test.TestService" service. /// Generated client protocol for the "test.TestService" service.
/// ///
@ -812,6 +850,7 @@ struct ProtobufCodeGeneratorTests {
} }
// Helpers providing default arguments to 'ClientProtocol' methods. // Helpers providing default arguments to 'ClientProtocol' methods.
@available(\(expectedAvailability), *)
extension Test_TestService.ClientProtocol { extension Test_TestService.ClientProtocol {
/// Call the "Unary" method. /// Call the "Unary" method.
/// ///
@ -927,6 +966,7 @@ struct ProtobufCodeGeneratorTests {
} }
// Helpers providing sugared APIs for 'ClientProtocol' methods. // Helpers providing sugared APIs for 'ClientProtocol' methods.
@available(\(expectedAvailability), *)
extension Test_TestService.ClientProtocol { extension Test_TestService.ClientProtocol {
/// Call the "Unary" method. /// Call the "Unary" method.
/// ///
@ -1062,6 +1102,36 @@ struct ProtobufCodeGeneratorTests {
#expect(generated == expected) #expect(generated == expected)
} }
@Test("Generate with different module names")
@available(gRPCSwiftProtobuf 2.0, *)
func generateWithDifferentModuleNames() throws {
var config = ProtobufCodeGenerator.Config.defaults
let defaultNames = config.moduleNames
config.accessLevel = .public
config.indentation = 2
config.moduleNames.grpcCore = String(config.moduleNames.grpcCore.reversed())
config.moduleNames.grpcProtobuf = String(config.moduleNames.grpcProtobuf.reversed())
config.moduleNames.swiftProtobuf = String(config.moduleNames.swiftProtobuf.reversed())
let generator = ProtobufCodeGenerator(config: config)
let generated = try generator.generateCode(
fileDescriptor: Self.fileDescriptor,
protoFileModuleMappings: ProtoFileToModuleMappings(),
extraModuleImports: []
)
// Mustn't contain the default names.
#expect(!generated.contains(defaultNames.grpcCore))
#expect(!generated.contains(defaultNames.grpcProtobuf))
#expect(!generated.contains(defaultNames.swiftProtobuf))
// Must contain the configured names.
#expect(generated.contains(config.moduleNames.grpcCore))
#expect(generated.contains(config.moduleNames.grpcProtobuf))
#expect(generated.contains(config.moduleNames.swiftProtobuf))
}
} }
@Suite("File-without-services (foo-messages.proto)") @Suite("File-without-services (foo-messages.proto)")
@ -1070,16 +1140,13 @@ struct ProtobufCodeGeneratorTests {
static let fileDescriptorName = "foo-messages" static let fileDescriptorName = "foo-messages"
@Test("Generate") @Test("Generate")
@available(gRPCSwiftProtobuf 2.0, *)
func generate() throws { func generate() throws {
let generator = ProtobufCodeGenerator( var config: ProtobufCodeGenerator.Config = .defaults
config: CodeGenerator.Config( config.accessLevel = .public
accessLevel: .public, config.indentation = 2
accessLevelOnImports: false,
client: true, let generator = ProtobufCodeGenerator(config: config)
server: true,
indentation: 2
)
)
let generated = try generator.generateCode( let generated = try generator.generateCode(
fileDescriptor: Self.fileDescriptor, fileDescriptor: Self.fileDescriptor,
@ -1092,6 +1159,7 @@ struct ProtobufCodeGeneratorTests {
// DO NOT EDIT. // DO NOT EDIT.
// swift-format-ignore-file // swift-format-ignore-file
// swiftlint:disable all
// //
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: foo-messages.proto // Source: foo-messages.proto

View File

@ -63,11 +63,12 @@ private func loadDescriptorSet(
) )
let url = try #require(maybeURL) let url = try #require(maybeURL)
let data = try #require(try Data(contentsOf: url)) let data = try Data(contentsOf: url)
let descriptorSet = try Google_Protobuf_FileDescriptorSet(serializedBytes: data) let descriptorSet = try Google_Protobuf_FileDescriptorSet(serializedBytes: data)
return DescriptorSet(proto: descriptorSet) return DescriptorSet(proto: descriptorSet)
} }
@available(gRPCSwiftProtobuf 2.0, *)
func parseDescriptor( func parseDescriptor(
_ descriptor: FileDescriptor, _ descriptor: FileDescriptor,
extraModuleImports: [String] = [], extraModuleImports: [String] = [],
@ -76,7 +77,8 @@ func parseDescriptor(
let parser = ProtobufCodeGenParser( let parser = ProtobufCodeGenParser(
protoFileModuleMappings: .init(), protoFileModuleMappings: .init(),
extraModuleImports: extraModuleImports, extraModuleImports: extraModuleImports,
accessLevel: accessLevel accessLevel: accessLevel,
moduleNames: .defaults
) )
return try parser.parse(descriptor: descriptor) return try parser.parse(descriptor: descriptor)
} }

View File

@ -40,6 +40,7 @@ struct DetailedErrorTests {
(["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]), (["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]),
] as [([String], [ErrorDetails])] ] as [([String], [ErrorDetails])]
) )
@available(gRPCSwiftProtobuf 2.0, *)
func rpcStatus(details: [String], expected: [ErrorDetails]) async throws { func rpcStatus(details: [String], expected: [ErrorDetails]) async throws {
let inProcess = InProcessTransport() let inProcess = InProcessTransport()
try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in
@ -95,11 +96,27 @@ struct DetailedErrorTests {
(.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#), (.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#),
] as [(ErrorDetails, String)] ] as [(ErrorDetails, String)]
) )
@available(gRPCSwiftProtobuf 2.0, *)
func errorInfoDescription(_ details: ErrorDetails, expected: String) { func errorInfoDescription(_ details: ErrorDetails, expected: String) {
#expect(String(describing: details) == expected) #expect(String(describing: details) == expected)
} }
@Test("Round-trip encoding of GoogleRPCStatus")
@available(gRPCSwiftProtobuf 2.0, *)
func googleRPCStatusRoundTripCoding() throws {
let detail = ErrorDetails.BadRequest(violations: [.init(field: "foo", description: "bar")])
let status = GoogleRPCStatus(code: .dataLoss, message: "Uh oh", details: [.badRequest(detail)])
let serialized: [UInt8] = try status.serializedBytes()
let deserialized = try GoogleRPCStatus(serializedBytes: serialized)
#expect(deserialized.code == status.code)
#expect(deserialized.message == status.message)
#expect(deserialized.details.count == status.details.count)
#expect(deserialized.details.first?.badRequest == detail)
}
} }
@available(gRPCSwiftProtobuf 2.0, *)
private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol { private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
func throwError( func throwError(
request: ThrowInput, request: ThrowInput,
@ -170,14 +187,17 @@ private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ErrorInfo { extension ErrorDetails.ErrorInfo {
fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"]) fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"])
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.RetryInfo { extension ErrorDetails.RetryInfo {
fileprivate static let testValue = Self(delay: .seconds(1)) fileprivate static let testValue = Self(delay: .seconds(1))
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.DebugInfo { extension ErrorDetails.DebugInfo {
fileprivate static let testValue = Self( fileprivate static let testValue = Self(
stack: ["foo.foo()", "foo.bar()"], stack: ["foo.foo()", "foo.bar()"],
@ -185,6 +205,7 @@ extension ErrorDetails.DebugInfo {
) )
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.QuotaFailure { extension ErrorDetails.QuotaFailure {
fileprivate static let testValue = Self( fileprivate static let testValue = Self(
violations: [ violations: [
@ -193,6 +214,7 @@ extension ErrorDetails.QuotaFailure {
) )
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.PreconditionFailure { extension ErrorDetails.PreconditionFailure {
fileprivate static let testValue = Self( fileprivate static let testValue = Self(
violations: [ violations: [
@ -201,6 +223,7 @@ extension ErrorDetails.PreconditionFailure {
) )
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.BadRequest { extension ErrorDetails.BadRequest {
fileprivate static let testValue = Self( fileprivate static let testValue = Self(
violations: [ violations: [
@ -209,14 +232,17 @@ extension ErrorDetails.BadRequest {
) )
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.RequestInfo { extension ErrorDetails.RequestInfo {
fileprivate static let testValue = Self(requestID: "id", servingData: "d") fileprivate static let testValue = Self(requestID: "id", servingData: "d")
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ResourceInfo { extension ErrorDetails.ResourceInfo {
fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d") fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d")
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.Help { extension ErrorDetails.Help {
fileprivate static let testValue = Self( fileprivate static let testValue = Self(
links: [ links: [
@ -225,6 +251,7 @@ extension ErrorDetails.Help {
) )
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.LocalizedMessage { extension ErrorDetails.LocalizedMessage {
fileprivate static let testValue = Self(locale: "l", message: "m") fileprivate static let testValue = Self(locale: "l", message: "m")
} }

View File

@ -15,6 +15,7 @@
// DO NOT EDIT. // DO NOT EDIT.
// swift-format-ignore-file // swift-format-ignore-file
// swiftlint:disable all
// //
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: error-service.proto // Source: error-service.proto
@ -29,6 +30,7 @@ internal import SwiftProtobuf
// MARK: - ErrorService // MARK: - ErrorService
/// Namespace containing generated types for the "ErrorService" service. /// Namespace containing generated types for the "ErrorService" service.
@available(gRPCSwiftProtobuf 2.0, *)
internal enum ErrorService { internal enum ErrorService {
/// Service descriptor for the "ErrorService" service. /// Service descriptor for the "ErrorService" service.
internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ErrorService") internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ErrorService")
@ -53,6 +55,7 @@ internal enum ErrorService {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCore.ServiceDescriptor { extension GRPCCore.ServiceDescriptor {
/// Service descriptor for the "ErrorService" service. /// Service descriptor for the "ErrorService" service.
internal static let ErrorService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ErrorService") internal static let ErrorService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "ErrorService")
@ -60,6 +63,7 @@ extension GRPCCore.ServiceDescriptor {
// MARK: ErrorService (server) // MARK: ErrorService (server)
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService { extension ErrorService {
/// Streaming variant of the service protocol for the "ErrorService" service. /// Streaming variant of the service protocol for the "ErrorService" service.
/// ///
@ -133,6 +137,7 @@ extension ErrorService {
} }
// Default implementation of 'registerMethods(with:)'. // Default implementation of 'registerMethods(with:)'.
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService.StreamingServiceProtocol { extension ErrorService.StreamingServiceProtocol {
internal func registerMethods<Transport>(with router: inout GRPCCore.RPCRouter<Transport>) where Transport: GRPCCore.ServerTransport { internal func registerMethods<Transport>(with router: inout GRPCCore.RPCRouter<Transport>) where Transport: GRPCCore.ServerTransport {
router.registerHandler( router.registerHandler(
@ -150,6 +155,7 @@ extension ErrorService.StreamingServiceProtocol {
} }
// Default implementation of streaming methods from 'StreamingServiceProtocol'. // Default implementation of streaming methods from 'StreamingServiceProtocol'.
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService.ServiceProtocol { extension ErrorService.ServiceProtocol {
internal func throwError( internal func throwError(
request: GRPCCore.StreamingServerRequest<ThrowInput>, request: GRPCCore.StreamingServerRequest<ThrowInput>,
@ -164,6 +170,7 @@ extension ErrorService.ServiceProtocol {
} }
// Default implementation of methods from 'ServiceProtocol'. // Default implementation of methods from 'ServiceProtocol'.
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService.SimpleServiceProtocol { extension ErrorService.SimpleServiceProtocol {
internal func throwError( internal func throwError(
request: GRPCCore.ServerRequest<ThrowInput>, request: GRPCCore.ServerRequest<ThrowInput>,
@ -181,6 +188,7 @@ extension ErrorService.SimpleServiceProtocol {
// MARK: ErrorService (client) // MARK: ErrorService (client)
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService { extension ErrorService {
/// Generated client protocol for the "ErrorService" service. /// Generated client protocol for the "ErrorService" service.
/// ///
@ -256,6 +264,7 @@ extension ErrorService {
} }
// Helpers providing default arguments to 'ClientProtocol' methods. // Helpers providing default arguments to 'ClientProtocol' methods.
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService.ClientProtocol { extension ErrorService.ClientProtocol {
/// Call the "ThrowError" method. /// Call the "ThrowError" method.
/// ///
@ -284,6 +293,7 @@ extension ErrorService.ClientProtocol {
} }
// Helpers providing sugared APIs for 'ClientProtocol' methods. // Helpers providing sugared APIs for 'ClientProtocol' methods.
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorService.ClientProtocol { extension ErrorService.ClientProtocol {
/// Call the "ThrowError" method. /// Call the "ThrowError" method.
/// ///

View File

@ -19,6 +19,7 @@ import GRPCProtobuf
import SwiftProtobuf import SwiftProtobuf
import XCTest import XCTest
@available(gRPCSwiftProtobuf 2.0, *)
final class ProtobufCodingTests: XCTestCase { final class ProtobufCodingTests: XCTestCase {
func testSerializeDeserializeRoundtrip() throws { func testSerializeDeserializeRoundtrip() throws {
let message = Google_Protobuf_Timestamp.with { let message = Google_Protobuf_Timestamp.with {
@ -73,6 +74,7 @@ final class ProtobufCodingTests: XCTestCase {
} }
} }
@available(gRPCSwiftProtobuf 2.0, *)
struct TestMessage: SwiftProtobuf.Message { struct TestMessage: SwiftProtobuf.Message {
var text: String = "" var text: String = ""
var unknownFields = SwiftProtobuf.UnknownStorage() var unknownFields = SwiftProtobuf.UnknownStorage()

View File

@ -21,12 +21,12 @@ protoc=$(which protoc)
# Checkout and build the plugins. # Checkout and build the plugins.
swift build --package-path "$root" --product protoc-gen-swift swift build --package-path "$root" --product protoc-gen-swift
swift build --package-path "$root" --product protoc-gen-grpc-swift swift build --package-path "$root" --product protoc-gen-grpc-swift-2
# Grab the plugin paths. # Grab the plugin paths.
bin_path=$(swift build --package-path "$root" --show-bin-path) bin_path=$(swift build --package-path "$root" --show-bin-path)
protoc_gen_swift="$bin_path/protoc-gen-swift" protoc_gen_swift="$bin_path/protoc-gen-swift"
protoc_gen_grpc_swift="$bin_path/protoc-gen-grpc-swift" protoc_gen_grpc_swift="$bin_path/protoc-gen-grpc-swift-2"
# Generates gRPC by invoking protoc with the gRPC Swift plugin. # Generates gRPC by invoking protoc with the gRPC Swift plugin.
# Parameters: # Parameters:
@ -36,10 +36,10 @@ protoc_gen_grpc_swift="$bin_path/protoc-gen-grpc-swift"
# - $4 onwards: options to forward to the plugin # - $4 onwards: options to forward to the plugin
function generate_grpc { function generate_grpc {
local proto=$1 local proto=$1
local args=("--plugin=$protoc_gen_grpc_swift" "--proto_path=${2}" "--grpc-swift_out=${3}") local args=("--plugin=$protoc_gen_grpc_swift" "--proto_path=${2}" "--grpc-swift-2_out=${3}")
for option in "${@:4}"; do for option in "${@:4}"; do
args+=("--grpc-swift_opt=$option") args+=("--grpc-swift-2_opt=$option")
done done
invoke_protoc "${args[@]}" "$proto" invoke_protoc "${args[@]}" "$proto"
@ -94,7 +94,7 @@ function generate_error_service {
output="$root/Tests/GRPCProtobufTests/Errors/Generated" output="$root/Tests/GRPCProtobufTests/Errors/Generated"
generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "UseAccessLevelOnImports=true" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "UseAccessLevelOnImports=true"
generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "UseAccessLevelOnImports=true" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "UseAccessLevelOnImports=true" "Availability=gRPCSwiftProtobuf 2.0"
} }
#- DESCRIPTOR SETS ------------------------------------------------------------ #- DESCRIPTOR SETS ------------------------------------------------------------
@ -105,7 +105,9 @@ function generate_test_service_descriptor_set {
proto_path="$(dirname "$proto")" proto_path="$(dirname "$proto")"
output="$root/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb" output="$root/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb"
invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" --include_source_info invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" \
--include_imports \
--include_source_info
} }
function generate_foo_service_descriptor_set { function generate_foo_service_descriptor_set {

View File

@ -3,6 +3,9 @@ syntax = "proto3";
package test; package test;
// Using a WKT forces an "SwiftProtobuf" to be imported in generated code.
import "google/protobuf/any.proto";
// Service docs. // Service docs.
service TestService { service TestService {
// Unary docs. // Unary docs.
@ -15,5 +18,8 @@ service TestService {
rpc BidirectionalStreaming (stream TestInput) returns (stream TestOutput) {} rpc BidirectionalStreaming (stream TestInput) returns (stream TestOutput) {}
} }
message TestInput {} message TestInput {
google.protobuf.Any any = 1;
}
message TestOutput {} message TestOutput {}

View File

@ -24,73 +24,185 @@ output_directory="${PLUGIN_TESTS_OUTPUT_DIRECTORY:=$(mktemp -d)}"
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
grpc_swift_protobuf_directory="$(readlink -f "${here}/..")" grpc_swift_protobuf_directory="$(readlink -f "${here}/..")"
resources_directory="$(readlink -f "${grpc_swift_protobuf_directory}/IntegrationTests/PluginTests/Resources")" resources_directory="$(readlink -f "${grpc_swift_protobuf_directory}/IntegrationTests/PluginTests/Resources")"
config="${resources_directory}/Config"
sources="${resources_directory}/Sources"
protos="${resources_directory}/Protos"
scratch_directory="$(mktemp -d)" scratch_directory="$(mktemp -d)"
package_manifest="${scratch_directory}/Package.swift"
echo "Output directory: $output_directory" echo "Output directory: $output_directory"
echo "grpc-swift-protobuf directory: $grpc_swift_protobuf_directory" echo "grpc-swift-protobuf directory: $grpc_swift_protobuf_directory"
# modify Package.swift # modify Package.swift
cp "${resources_directory}/Package.swift" "${scratch_directory}/" cp "${resources_directory}/Sources/Package.swift" "${scratch_directory}/"
cat >> "${scratch_directory}/Package.swift" <<- EOM cat >> "${package_manifest}" <<- EOM
package.dependencies.append( package.dependencies.append(
.package(path: "$grpc_swift_protobuf_directory") .package(path: "$grpc_swift_protobuf_directory")
) )
EOM EOM
# test_01_top_level_config_file function test_dir_name {
test_01_output_directory="${output_directory}/test_01_top_level_config_file" # $FUNCNAME is a stack of function names. The 0th element is the name of this
mkdir -p "${test_01_output_directory}/Sources/Protos" # function, so the 1st element is the calling function.
cp "${scratch_directory}/Package.swift" "${test_01_output_directory}/" echo "${output_directory}/${FUNCNAME[1]}"
cp "${resources_directory}/HelloWorldAdopter.swift" "${test_01_output_directory}/Sources/adopter.swift" }
cp "${resources_directory}/HelloWorld/HelloWorld.proto" "${test_01_output_directory}/Sources/Protos"
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_01_output_directory}/Sources/grpc-swift-proto-generator-config.json"
# test_02_peer_config_file function test_01_top_level_config_file {
test_02_output_directory="${output_directory}/test_02_peer_config_file" # .
mkdir -p "${test_02_output_directory}/Sources/Protos" # ├── Package.swift
cp "${scratch_directory}/Package.swift" "${test_02_output_directory}/" # └── Sources
cp "${resources_directory}/HelloWorldAdopter.swift" "${test_02_output_directory}/Sources/adopter.swift" # ├── HelloWorldAdopter.swift
cp "${resources_directory}/HelloWorld/HelloWorld.proto" "${test_02_output_directory}/Sources/Protos/" # ├── Protos
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_02_output_directory}/Sources/Protos/grpc-swift-proto-generator-config.json" # │ └── HelloWorld.proto
# └── grpc-swift-proto-generator-config.json
# test_03_separate_service_message_protos local -r test_dir=$(test_dir_name)
test_03_output_directory="${output_directory}/test_03_separate_service_message_protos" mkdir -p "${test_dir}/Sources/Protos"
mkdir -p "${test_03_output_directory}/Sources/Protos" cp "${package_manifest}" "${test_dir}/"
cp "${scratch_directory}/Package.swift" "${test_03_output_directory}/" cp "${sources}/HelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${resources_directory}/HelloWorldAdopter.swift" "${test_03_output_directory}/Sources/adopter.swift" cp "${protos}/HelloWorld/HelloWorld.proto" "${test_dir}/Sources/Protos"
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_03_output_directory}/Sources/Protos/grpc-swift-proto-generator-config.json" cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/grpc-swift-proto-generator-config.json"
cp "${resources_directory}/HelloWorld/Service.proto" "${test_03_output_directory}/Sources/Protos/" }
cp "${resources_directory}/HelloWorld/Messages.proto" "${test_03_output_directory}/Sources/Protos/"
# test_04_cross_directory_imports function test_02_peer_config_file {
test_04_output_directory="${output_directory}/test_04_cross_directory_imports" # .
mkdir -p "${test_04_output_directory}/Sources/Protos/directory_1" # ├── Package.swift
mkdir -p "${test_04_output_directory}/Sources/Protos/directory_2" # └── Sources
cp "${scratch_directory}/Package.swift" "${test_04_output_directory}/" # ├── HelloWorldAdopter.swift
cp "${resources_directory}/HelloWorldAdopter.swift" "${test_04_output_directory}/Sources/adopter.swift" # └── Protos
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_04_output_directory}/Sources/Protos/directory_1/grpc-swift-proto-generator-config.json" # ├── HelloWorld.proto
cp "${resources_directory}/import-directory-1-grpc-swift-proto-generator-config.json" "${test_04_output_directory}/Sources/Protos/directory_2/grpc-swift-proto-generator-config.json" # └── grpc-swift-proto-generator-config.json
cp "${resources_directory}/HelloWorld/Service.proto" "${test_04_output_directory}/Sources/Protos/directory_2/"
cp "${resources_directory}/HelloWorld/Messages.proto" "${test_04_output_directory}/Sources/Protos/directory_1/"
# test_05_two_definitions local -r test_dir=$(test_dir_name)
test_05_output_directory="${output_directory}/test_05_two_definitions" mkdir -p "${test_dir}/Sources/Protos"
mkdir -p "${test_05_output_directory}/Sources/Protos/HelloWorld" cp "${package_manifest}" "${test_dir}/"
mkdir -p "${test_05_output_directory}/Sources/Protos/Foo" cp "${sources}/HelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${scratch_directory}/Package.swift" "${test_05_output_directory}/" cp "${protos}/HelloWorld/HelloWorld.proto" "${test_dir}/Sources/Protos/"
cp "${resources_directory}/FooHelloWorldAdopter.swift" "${test_05_output_directory}/Sources/adopter.swift" cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/grpc-swift-proto-generator-config.json"
cp "${resources_directory}/HelloWorld/HelloWorld.proto" "${test_05_output_directory}/Sources/Protos/HelloWorld/" }
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_05_output_directory}/Sources/Protos/grpc-swift-proto-generator-config.json"
cp "${resources_directory}/Foo/foo-messages.proto" "${test_05_output_directory}/Sources/Protos/Foo/"
cp "${resources_directory}/Foo/foo-service.proto" "${test_05_output_directory}/Sources/Protos/Foo/"
# test_06_nested_definitions function test_03_separate_service_message_protos {
test_06_output_directory="${output_directory}/test_06_nested_definitions" # .
mkdir -p "${test_06_output_directory}/Sources/Protos/HelloWorld/FooDefinitions/Foo" # ├── Package.swift
cp "${scratch_directory}/Package.swift" "${test_06_output_directory}/" # └── Sources
cp "${resources_directory}/FooHelloWorldAdopter.swift" "${test_06_output_directory}/Sources/adopter.swift" # ├── HelloWorldAdopter.swift
cp "${resources_directory}/HelloWorld/HelloWorld.proto" "${test_06_output_directory}/Sources/Protos/HelloWorld/" # └── Protos
cp "${resources_directory}/internal-grpc-swift-proto-generator-config.json" "${test_06_output_directory}/Sources/Protos/HelloWorld/grpc-swift-proto-generator-config.json" # ├── Messages.proto
cp "${resources_directory}/public-grpc-swift-proto-generator-config.json" "${test_06_output_directory}/Sources/Protos/HelloWorld/FooDefinitions/grpc-swift-proto-generator-config.json" # ├── Service.proto
cp "${resources_directory}/Foo/foo-messages.proto" "${test_06_output_directory}/Sources/Protos/HelloWorld/FooDefinitions/Foo/" # └── grpc-swift-proto-generator-config.json
cp "${resources_directory}/Foo/foo-service.proto" "${test_06_output_directory}/Sources/Protos/HelloWorld/FooDefinitions/Foo/"
local -r test_dir=$(test_dir_name)
mkdir -p "${test_dir}/Sources/Protos"
cp "${package_manifest}" "${test_dir}/"
cp "${sources}/HelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/grpc-swift-proto-generator-config.json"
cp "${protos}/HelloWorld/Service.proto" "${test_dir}/Sources/Protos/"
cp "${protos}/HelloWorld/Messages.proto" "${test_dir}/Sources/Protos/"
}
function test_04_cross_directory_imports {
# .
# ├── Package.swift
# └── Sources
# ├── HelloWorldAdopter.swift
# └── Protos
# ├── directory_1
# │ ├── Messages.proto
# │ └── grpc-swift-proto-generator-config.json
# └── directory_2
# ├── Service.proto
# └── grpc-swift-proto-generator-config.json
local -r test_dir=$(test_dir_name)
mkdir -p "${test_dir}/Sources/Protos/directory_1"
mkdir -p "${test_dir}/Sources/Protos/directory_2"
cp "${package_manifest}" "${test_dir}/"
cp "${sources}/HelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/directory_1/grpc-swift-proto-generator-config.json"
cp "${config}/import-directory-1-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/directory_2/grpc-swift-proto-generator-config.json"
cp "${protos}/HelloWorld/Service.proto" "${test_dir}/Sources/Protos/directory_2/"
cp "${protos}/HelloWorld/Messages.proto" "${test_dir}/Sources/Protos/directory_1/"
}
function test_05_two_definitions {
# .
# ├── Package.swift
# └── Sources
# ├── FooHelloWorldAdopter.swift
# └── Protos
# ├── Foo
# │ ├── foo-messages.proto
# │ └── foo-service.proto
# ├── HelloWorld
# │ └── HelloWorld.proto
# └── grpc-swift-proto-generator-config.json
local -r test_dir=$(test_dir_name)
mkdir -p "${test_dir}/Sources/Protos/HelloWorld"
mkdir -p "${test_dir}/Sources/Protos/Foo"
cp "${package_manifest}" "${test_dir}/"
cp "${sources}/FooHelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${protos}/HelloWorld/HelloWorld.proto" "${test_dir}/Sources/Protos/HelloWorld/"
cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/grpc-swift-proto-generator-config.json"
cp "${protos}/Foo/foo-messages.proto" "${test_dir}/Sources/Protos/Foo/"
cp "${protos}/Foo/foo-service.proto" "${test_dir}/Sources/Protos/Foo/"
}
function test_06_nested_definitions {
# .
# ├── Package.swift
# └── Sources
# ├── FooHelloWorldAdopter.swift
# └── Protos
# └── HelloWorld
# ├── FooDefinitions
# │ ├── Foo
# │ │ ├── foo-messages.proto
# │ │ └── foo-service.proto
# │ └── grpc-swift-proto-generator-config.json
# ├── HelloWorld.proto
# └── grpc-swift-proto-generator-config.json
local -r test_dir=$(test_dir_name)
mkdir -p "${test_dir}/Sources/Protos/HelloWorld/FooDefinitions/Foo"
cp "${package_manifest}" "${test_dir}/"
cp "${sources}/FooHelloWorldAdopter.swift" "${test_dir}/Sources/"
cp "${protos}/HelloWorld/HelloWorld.proto" "${test_dir}/Sources/Protos/HelloWorld/"
cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/HelloWorld/grpc-swift-proto-generator-config.json"
cp "${config}/public-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/HelloWorld/FooDefinitions/grpc-swift-proto-generator-config.json"
cp "${protos}/Foo/foo-messages.proto" "${test_dir}/Sources/Protos/HelloWorld/FooDefinitions/Foo/"
cp "${protos}/Foo/foo-service.proto" "${test_dir}/Sources/Protos/HelloWorld/FooDefinitions/Foo/"
}
function test_07_duplicated_proto_file_name {
# .
# ├── Package.swift
# └── Sources
# ├── NoOp.swift
# └── Protos
# ├── grpc-swift-proto-generator-config.json
# ├── noop
# │ └── noop.proto
# └── noop2
# └── noop.proto
local -r test_dir=$(test_dir_name)
mkdir -p "${test_dir}/Sources/Protos"
cp "${package_manifest}" "${test_dir}/"
mkdir -p "${test_dir}/Sources/Protos"
cp -rp "${protos}/noop" "${test_dir}/Sources/Protos"
cp -rp "${protos}/noop2" "${test_dir}/Sources/Protos"
cp "${sources}/NoOp.swift" "${test_dir}/Sources"
cp "${config}/internal-grpc-swift-proto-generator-config.json" "${test_dir}/Sources/Protos/grpc-swift-proto-generator-config.json"
}
test_01_top_level_config_file
test_02_peer_config_file
test_03_separate_service_message_protos
test_04_cross_directory_imports
test_05_two_definitions
test_06_nested_definitions
test_07_duplicated_proto_file_name