Compare commits

...

25 Commits
1.0.0 ... main

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
58 changed files with 1655 additions and 453 deletions

View File

@ -13,9 +13,10 @@ jobs:
with:
linux_5_9_enabled: false
linux_5_10_enabled: false
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_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_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
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:
name: Construct plugin tests matrix
@ -32,7 +33,7 @@ jobs:
env:
MATRIX_LINUX_5_9_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"
plugin-tests-matrix:
@ -42,3 +43,7 @@ jobs:
with:
name: "Plugin tests"
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
with:
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:
name: Soundness
@ -22,9 +25,10 @@ jobs:
with:
linux_5_9_enabled: false
linux_5_10_enabled: false
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_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_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability"
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:
name: Construct plugin tests matrix
@ -41,7 +45,7 @@ jobs:
env:
MATRIX_LINUX_5_9_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"
plugin-tests-matrix:
@ -58,3 +62,7 @@ jobs:
with:
linux_5_9_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
run: |
./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,9 +28,8 @@ let package = Package(
],
dependencies: [
// Dependency on grpc-swift-protobuf to be added by setup-plugin-tests.sh script
.package(
url: "https://github.com/grpc/grpc-swift.git",
url: "https://github.com/grpc/grpc-swift-2.git",
from: "2.0.0"
)
],
@ -38,8 +37,8 @@ let package = Package(
.executableTarget(
name: "grpc-adopter",
dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
.product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift-2"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
],
plugins: [

View File

@ -23,18 +23,22 @@ let products: [Product] = [
targets: ["GRPCProtobuf"]
),
.executable(
name: "protoc-gen-grpc-swift",
targets: ["protoc-gen-grpc-swift"]
name: "protoc-gen-grpc-swift-2",
targets: ["protoc-gen-grpc-swift-2"]
),
.plugin(
name: "GRPCProtobufGenerator",
targets: ["GRPCProtobufGenerator"]
),
.plugin(
name: "generate-grpc-code-from-protos",
targets: ["generate-grpc-code-from-protos"]
),
]
let dependencies: [Package.Dependency] = [
.package(
url: "https://github.com/grpc/grpc-swift.git",
url: "https://github.com/grpc/grpc-swift-2.git",
from: "2.0.0"
),
.package(
@ -43,20 +47,36 @@ let dependencies: [Package.Dependency] = [
),
]
let defaultSwiftSettings: [SwiftSetting] = [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
]
// -------------------------------------------------------------------------------------------------
let targets: [Target] = [
// protoc plugin for grpc-swift
// This adds some build settings which allow us to map "@available(gRPCSwiftProtobuf 2.x, *)" to
// 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(
name: "protoc-gen-grpc-swift",
name: "protoc-gen-grpc-swift-2",
dependencies: [
.target(name: "GRPCProtobufCodeGen"),
.product(name: "GRPCCodeGen", package: "grpc-swift"),
.product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
],
@ -67,7 +87,7 @@ let targets: [Target] = [
.target(
name: "GRPCProtobuf",
dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
swiftSettings: defaultSwiftSettings
@ -76,18 +96,18 @@ let targets: [Target] = [
name: "GRPCProtobufTests",
dependencies: [
.target(name: "GRPCProtobuf"),
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
.product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
swiftSettings: defaultSwiftSettings
),
// Code generator library for protoc-gen-grpc-swift
// Code generator library for protoc-gen-grpc-swift-2
.target(
name: "GRPCProtobufCodeGen",
dependencies: [
.product(name: "GRPCCodeGen", package: "grpc-swift"),
.product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
],
swiftSettings: defaultSwiftSettings
@ -96,7 +116,7 @@ let targets: [Target] = [
name: "GRPCProtobufCodeGenTests",
dependencies: [
.target(name: "GRPCProtobufCodeGen"),
.product(name: "GRPCCodeGen", package: "grpc-swift"),
.product(name: "GRPCCodeGen", package: "grpc-swift-2"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
],
@ -111,21 +131,81 @@ let targets: [Target] = [
name: "GRPCProtobufGenerator",
capability: .buildTool(),
dependencies: [
.target(name: "protoc-gen-grpc-swift"),
.target(name: "protoc-gen-grpc-swift-2"),
.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(
name: "grpc-swift-protobuf",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v11),
.visionOS(.v2),
],
products: products,
dependencies: dependencies,
targets: targets

View File

@ -16,8 +16,6 @@
import Foundation
let configFileName = "grpc-swift-proto-generator-config.json"
/// The config of the build plugin.
struct BuildPluginConfig: Codable {
/// Config defining which components should be considered when generating source.
@ -193,12 +191,14 @@ extension BuildPluginConfig.Protoc: Codable {
extension GenerationConfig {
init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) {
self.server = buildPluginConfig.generate.servers
self.client = buildPluginConfig.generate.clients
self.message = buildPluginConfig.generate.messages
// hard-code full-path to avoid collisions since this goes into a temporary directory anyway
self.fileNaming = .fullPath
self.visibility = buildPluginConfig.generatedSource.accessLevel
self.servers = buildPluginConfig.generate.servers
self.clients = buildPluginConfig.generate.clients
self.messages = buildPluginConfig.generate.messages
// Use path to underscores as it ensures output files are unique (files generated from
// "foo/bar.proto" won't collide with those generated from "bar/bar.proto" as they'll be
// 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
// Generate absolute paths for the imports relative to the config file in which they are specified
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");
* you may not use this file except in compliance with the License.
@ -14,13 +14,12 @@
* limitations under the License.
*/
enum PluginError: Error {
// Build plugin
enum BuildPluginError: Error {
case incompatibleTarget(String)
case noConfigFilesFound
}
extension PluginError: CustomStringConvertible {
extension BuildPluginError: CustomStringConvertible {
var description: String {
switch self {
case .incompatibleTarget(let target):

View File

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

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`
/// - `PathToUnderscore`: `foo_bar_baz.grpc.swift`
/// - `DropPath`: `baz.grpc.swift`
enum FileNaming: String, Codable {
enum FileNaming: String {
/// Replicate the input file path with the output file(s).
case fullPath = "FullPath"
/// Convert path directory delimiters to underscores.
@ -42,13 +42,13 @@ struct GenerationConfig {
}
/// The visibility of the generated files.
var visibility: AccessLevel
var accessLevel: AccessLevel
/// Whether server code is generated.
var server: Bool
var servers: Bool
/// Whether client code is generated.
var client: Bool
var clients: Bool
/// 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.
var fileNaming: FileNaming
/// 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 PackagePlugin
let configFileName = "grpc-swift-proto-generator-config.json"
/// Derive the path to the instance of `protoc` to be used.
/// - Parameters:
/// - 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("--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=UseAccessLevelOnImports=\(config.accessLevelOnImports)")
protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme })
@ -71,15 +73,15 @@ func constructProtocGenSwiftArguments(
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:
/// - config: The config for this operation.
/// - fileNaming: The file naming scheme to be used.
/// - inputFiles: The input `.proto` files.
/// - 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.
/// - 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(
config: GenerationConfig,
fileNaming: GenerationConfig.FileNaming?,
@ -97,9 +99,9 @@ func constructProtocGenGRPCSwiftArguments(
protocArgs.append("--proto_path=\(path)")
}
protocArgs.append("--grpc-swift_opt=Visibility=\(config.visibility.rawValue.capitalized)")
protocArgs.append("--grpc-swift_opt=Server=\(config.server)")
protocArgs.append("--grpc-swift_opt=Client=\(config.client)")
protocArgs.append("--grpc-swift_opt=Visibility=\(config.accessLevel.rawValue.capitalized)")
protocArgs.append("--grpc-swift_opt=Server=\(config.servers)")
protocArgs.append("--grpc-swift_opt=Client=\(config.clients)")
protocArgs.append("--grpc-swift_opt=FileNaming=\(config.fileNaming.rawValue)")
protocArgs.append("--grpc-swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)")
protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme })
@ -117,3 +119,14 @@ extension URL {
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-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

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
/// Serializes a Protobuf message into a sequence of bytes.
@available(gRPCSwiftProtobuf 2.0, *)
public struct ProtobufSerializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageSerializer {
public init() {}
@ -41,6 +42,7 @@ public struct ProtobufSerializer<Message: SwiftProtobuf.Message>: GRPCCore.Messa
}
/// Deserializes a sequence of bytes into a Protobuf message.
@available(gRPCSwiftProtobuf 2.0, *)
public struct ProtobufDeserializer<Message: SwiftProtobuf.Message>: GRPCCore.MessageDeserializer {
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
/// refine `SwiftProtobufContiguousBytes` for the same reason.
@usableFromInline
@available(gRPCSwiftProtobuf 2.0, *)
struct ContiguousBytesAdapter<
Bytes: GRPCContiguousBytes
>: GRPCContiguousBytes, SwiftProtobufContiguousBytes {

View File

@ -9,7 +9,7 @@ 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` plugin for `protoc`).
- 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
@ -73,6 +73,6 @@ 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` and
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": {
"clients": true,
"servers": true,
"messages": true,
"messages": true
},
"generatedSource": {
"accessLevelOnImports": false,
"accessLevel": "internal",
}
"protoc": {
"executablePath": "/opt/homebrew/bin/protoc"
"importPaths": [
"../directory_1",
],
"accessLevel": "internal"
},
"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.
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.
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
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
the `--grpc-swift_out=<DIRECTORY>` option:
the `--grpc-swift-2_out=<DIRECTORY>` option:
```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.
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
explicitly:
```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
the `--grpc-swift_opt` argument:
You can also specify various option the `protoc-gen-grpc-swift-2` via `protoc` using
the `--grpc-swift-2_opt` argument:
```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
protoc \
--grpc-swift_opt=<OPTION_NAME1>=<OPTION_VALUE1> \
--grpc-swift_opt=<OPTION_NAME2>=<OPTION_VALUE2> \
--grpc-swift_out=.
--grpc-swift-2_opt=<OPTION_NAME1>=<OPTION_VALUE1> \
--grpc-swift-2_opt=<OPTION_NAME2>=<OPTION_VALUE2> \
--grpc-swift-2_out=.
```
#### Generator options
| Name | Possible Values | Default | Description |
|---------------------------|--------------------------------------------|------------|----------------------------------------------------------|
| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs |
| `Server` | `True`, `False` | `True` | Generate server stubs |
| `Client` | `True`, `False` | `True` | Generate client stubs |
| `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) |
| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) |
| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. |
| Name | Possible Values | Default | Description |
|---------------------------|---------------------------------------------|-----------------|----------------------------------------------------------|
| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs |
| `Server` | `True`, `False` | `True` | Generate server stubs |
| `Client` | `True`, `False` | `True` | Generate client stubs |
| `FileNaming` | `FullPath`, `PathToUnderscores`, `DropPath` | `FullPath` | How generated source files should be named. † |
| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. ‡ |
| `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:
- `FullPath`: `foo/bar/baz.grpc.swift`.
- `PathToUnderscore`: `foo_bar_baz.grpc.swift`
- `PathToUnderscores`: `foo_bar_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 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).
§ 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
> 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.
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:
```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

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

@ -1,12 +1,12 @@
# Understanding the generated code
Understand what code is generated by `protoc-gen-grpc-swift` from a `.proto`
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`. The plugin is responsible
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

View File

@ -7,7 +7,7 @@ A package integrating Swift Protobuf with gRPC Swift.
This package provides three products:
- ``GRPCProtobuf``, a module providing runtime serialization and deserialization components for
[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
`grpc-swift` documentation on the [Swift Package
Index](https://swiftpackageindex.com/grpc/grpc-swift/documentation).
@ -23,6 +23,7 @@ This package provides three products:
- <doc:Generating-stubs>
- <doc:API-stability-of-generated-code>
- <doc:Understanding-the-generated-code>
- <doc:Public-services-with-private-implementations>
### Serialization

View File

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

View File

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

View File

@ -16,6 +16,7 @@
internal import SwiftProtobuf
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails {
/// 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"
/// protobuf (`Google_Protobuf_Any`).
@available(gRPCSwiftProtobuf 2.0, *)
public struct ErrorDetails: Sendable, Hashable {
enum Wrapped: Sendable, Hashable {
case errorInfo(ErrorInfo)
@ -198,6 +199,7 @@ public struct ErrorDetails: Sendable, Hashable {
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails {
/// Returns error info if set.
public var errorInfo: ErrorInfo? {

View File

@ -15,7 +15,7 @@
*/
public import GRPCCore
internal import SwiftProtobuf
public import SwiftProtobuf
/// 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
/// > 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
/// > is a "google.rpc.Status" protocol buffers message containing the status code, message, and
/// > details.
public struct GoogleRPCStatus: Error {
/// > the serialized bytes of a "google.rpc.Status" protocol buffers message containing the status
/// > code, message, and details.
@available(gRPCSwiftProtobuf 2.0, *)
public struct GoogleRPCStatus: Error, Hashable {
/// A code representing the high-level domain of the error.
public var code: RPCError.Code
@ -74,13 +74,33 @@ public struct GoogleRPCStatus: Error {
}
}
extension GoogleRPCStatus: GoogleProtobufAnyPackable {
// See https://protobuf.dev/programming-guides/proto3/#any
internal static var typeURL: String { "type.googleapis.com/google.rpc.Status" }
init?(unpacking any: Google_Protobuf_Any) throws {
guard any.isA(Google_Rpc_Status.self) else { return nil }
let status = try Google_Rpc_Status(serializedBytes: any.value)
@available(gRPCSwiftProtobuf 2.0, *)
extension GoogleRPCStatus {
/// Creates a new message by decoding the given `SwiftProtobufContiguousBytes` value
/// containing a serialized message in Protocol Buffer binary format.
///
/// - Parameters:
/// - 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))
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) }
}
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 {
$0.code = Int32(self.code.rawValue)
$0.message = self.message
$0.details = try self.details.map { try $0.pack() }
}
return try .with {
$0.typeURL = Self.typeURL
$0.value = try status.serializedBytes()
}
return try status.serializedBytes(partial: partial, options: options)
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension GoogleRPCStatus: RPCErrorConvertible {
public var rpcErrorCode: RPCError.Code { self.code }
public var rpcErrorMessage: String { self.message }
public var rpcErrorMetadata: Metadata {
do {
let any = try self.pack()
let bytes: [UInt8] = try any.serializedBytes()
let bytes: [UInt8] = try self.serializedBytes()
return [Metadata.statusDetailsBinKey: .binary(bytes)]
} catch {
// Failed to serialize error details. Not a lot can be done here.

View File

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

View File

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

View File

@ -17,11 +17,12 @@
package import GRPCCodeGen
package import SwiftProtobufPluginLibrary
@available(gRPCSwiftProtobuf 2.0, *)
package struct ProtobufCodeGenerator {
internal var config: GRPCCodeGen.CodeGenerator.Config
internal var config: ProtobufCodeGenerator.Config
package init(
config: GRPCCodeGen.CodeGenerator.Config
config: ProtobufCodeGenerator.Config
) {
self.config = config
}
@ -29,17 +30,76 @@ package struct ProtobufCodeGenerator {
package func generateCode(
fileDescriptor: FileDescriptor,
protoFileModuleMappings: ProtoFileToModuleMappings,
extraModuleImports: [String]
extraModuleImports: [String],
availabilityOverrides: [(os: String, version: String)] = []
) throws -> String {
let parser = ProtobufCodeGenParser(
protoFileModuleMappings: protoFileModuleMappings,
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 sourceFile = try codeGenerator.generate(codeGenerationRequest)
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
@main
@available(gRPCSwiftProtobuf 2.0, *)
final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
var version: String? {
Version.versionString
@ -55,37 +56,10 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
let options = try GeneratorOptions(parameter: parameter)
for descriptor in fileDescriptors {
if options.generateReflectionData {
try self.generateReflectionData(
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(
_ descriptor: FileDescriptor,
options: GeneratorOptions,
@ -96,18 +70,19 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
fileNamingOption: options.fileNaming
)
let config = CodeGenerator.Config(options: options)
let fileGenerator = ProtobufCodeGenerator(config: config)
let fileGenerator = ProtobufCodeGenerator(config: options.config)
let contents = try fileGenerator.generateCode(
fileDescriptor: descriptor,
protoFileModuleMappings: options.protoToModuleMappings,
extraModuleImports: options.extraModuleImports
extraModuleImports: options.extraModuleImports,
availabilityOverrides: options.availabilityOverrides
)
try outputs.add(fileName: fileName, contents: contents)
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension GenerateGRPC {
private func uniqueOutputFileName(
fileDescriptor: FileDescriptor,
@ -181,24 +156,3 @@ private func splitPath(pathname: String) -> (dir: String, base: String, 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.
*/
import GRPCCodeGen
import GRPCProtobufCodeGen
import SwiftProtobufPluginLibrary
enum GenerationError: Error, CustomStringConvertible {
@ -23,6 +25,8 @@ enum GenerationError: Error, CustomStringConvertible {
case invalidParameterValue(name: String, value: String)
/// Raised to wrap another error but provide a context message.
case wrappedError(message: String, error: any Error)
/// The parameter isn't supported.
case unsupportedParameter(name: String, message: String)
var description: String {
switch self {
@ -32,6 +36,8 @@ enum GenerationError: Error, CustomStringConvertible {
return "Unknown value for generation parameter '\(name)': '\(value)'"
case let .wrappedError(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"
}
@available(gRPCSwiftProtobuf 2.0, *)
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 fileNaming = FileNaming.fullPath
private(set) var extraModuleImports: [String] = []
private(set) var gRPCModuleName = "GRPC"
private(set) var swiftProtobufModuleName = "SwiftProtobuf"
private(set) var generateReflectionData = false
private(set) var useAccessLevelOnImports = false
private(set) var availabilityOverrides: [(os: String, version: String)] = []
private(set) var config: ProtobufCodeGenerator.Config = .defaults
init(parameter: any CodeGeneratorParameter) throws {
try self.init(pairs: parameter.parsedPairs)
@ -81,22 +65,22 @@ struct GeneratorOptions {
for pair in pairs {
switch pair.key {
case "Visibility":
if let value = Visibility(rawValue: pair.value) {
self.visibility = value
if let value = GRPCCodeGen.CodeGenerator.Config.AccessLevel(protocOption: pair.value) {
self.config.accessLevel = value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "Server":
if let value = Bool(pair.value.lowercased()) {
self.generateServer = value
self.config.generateServer = value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "Client":
if let value = Bool(pair.value.lowercased()) {
self.generateClient = value
self.config.generateClient = value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
@ -129,28 +113,49 @@ struct GeneratorOptions {
case "GRPCModuleName":
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 {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "SwiftProtobufModuleName":
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 {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
case "ReflectionData":
if let value = Bool(pair.value.lowercased()) {
self.generateReflectionData = value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
throw GenerationError.unsupportedParameter(
name: pair.key,
message: """
The reflection service uses descriptor sets. Refer to the protoc docs and the \
'--descriptor_set_out' option for more information.
"""
)
case "UseAccessLevelOnImports":
if let value = Bool(pair.value.lowercased()) {
self.useAccessLevelOnImports = value
self.config.accessLevelOnImports = value
} else {
throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
}
@ -187,6 +192,7 @@ struct GeneratorOptions {
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension String.SubSequence {
func trimmingWhitespaceAndNewlines() -> String {
let trimmedSuffix = self.drop(while: { $0.isNewline || $0.isWhitespace })
@ -194,3 +200,19 @@ extension String.SubSequence {
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.
*/
#if canImport(CGRPCProtobuf)
private import CGRPCProtobuf
#endif
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.
internal static var versionString: String {
let version = "\(Self.major).\(Self.minor).\(Self.patch)"
if Self.label.isEmpty {
return version
} else {
return version + "-" + Self.label
}
#if canImport(CGRPCProtobuf)
String(cString: cgrprc_grpc_swift_protobuf_version())
#else
"unknown"
#endif
}
}

View File

@ -27,25 +27,29 @@ struct ProtobufCodeGenParserTests {
static let descriptorSetName = "test-service"
static let fileDescriptorName = "test-service"
let codeGen: CodeGenerationRequest
init() throws {
let descriptor = try #require(try Self.fileDescriptor)
self.codeGen = try parseDescriptor(descriptor)
@available(gRPCSwiftProtobuf 2.0, *)
var codeGen: CodeGenerationRequest {
get throws {
let descriptor = try Self.fileDescriptor
return try parseDescriptor(descriptor)
}
}
@Test("Filename")
func fileName() {
#expect(self.codeGen.fileName == "test-service.proto")
@available(gRPCSwiftProtobuf 2.0, *)
func fileName() throws {
#expect(try self.codeGen.fileName == "test-service.proto")
}
@Test("Leading trivia")
func leadingTrivia() {
@available(gRPCSwiftProtobuf 2.0, *)
func leadingTrivia() throws {
let expected = """
/// Leading trivia.
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: test-service.proto
@ -55,94 +59,112 @@ struct ProtobufCodeGenParserTests {
"""
#expect(self.codeGen.leadingTrivia == expected)
#expect(try self.codeGen.leadingTrivia == expected)
}
@Test("Dependencies")
func dependencies() {
@available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
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")
struct Service {
let service: GRPCCodeGen.ServiceDescriptor
init() throws {
let request = try parseDescriptor(try #require(try TestService.fileDescriptor))
try #require(request.services.count == 1)
self.service = try #require(request.services.first)
@available(gRPCSwiftProtobuf 2.0, *)
var service: GRPCCodeGen.ServiceDescriptor {
get throws {
let request = try parseDescriptor(try TestService.fileDescriptor)
try #require(request.services.count == 1)
return try #require(request.services.first)
}
}
@Test("Name")
func name() {
#expect(self.service.name.identifyingName == "test.TestService")
@available(gRPCSwiftProtobuf 2.0, *)
func name() throws {
#expect(try self.service.name.identifyingName == "test.TestService")
}
@Suite("Methods")
struct Methods {
let unary: GRPCCodeGen.MethodDescriptor
let clientStreaming: GRPCCodeGen.MethodDescriptor
let serverStreaming: GRPCCodeGen.MethodDescriptor
let bidiStreaming: GRPCCodeGen.MethodDescriptor
@available(gRPCSwiftProtobuf 2.0, *)
var service: GRPCCodeGen.ServiceDescriptor {
get throws {
let request = try parseDescriptor(try TestService.fileDescriptor)
try #require(request.services.count == 1)
return try #require(request.services.first)
}
}
init() throws {
let request = try parseDescriptor(try #require(try TestService.fileDescriptor))
#expect(request.services.count == 1)
let service = try #require(request.services.first)
self.unary = service.methods[0]
self.clientStreaming = service.methods[1]
self.serverStreaming = service.methods[2]
self.bidiStreaming = service.methods[3]
@available(gRPCSwiftProtobuf 2.0, *)
var unary: GRPCCodeGen.MethodDescriptor {
get throws { try self.service.methods[0] }
}
@available(gRPCSwiftProtobuf 2.0, *)
var clientStreaming: GRPCCodeGen.MethodDescriptor {
get throws { try self.service.methods[1] }
}
@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")
func documentation() {
#expect(self.unary.documentation == "/// Unary docs.\n")
#expect(self.clientStreaming.documentation == "/// Client streaming docs.\n")
#expect(self.serverStreaming.documentation == "/// Server streaming docs.\n")
#expect(self.bidiStreaming.documentation == "/// Bidirectional streaming docs.\n")
@available(gRPCSwiftProtobuf 2.0, *)
func documentation() throws {
#expect(try self.unary.documentation == "/// Unary docs.\n")
#expect(try self.clientStreaming.documentation == "/// Client streaming docs.\n")
#expect(try self.serverStreaming.documentation == "/// Server streaming docs.\n")
#expect(try self.bidiStreaming.documentation == "/// Bidirectional streaming docs.\n")
}
@Test("Name")
func name() {
#expect(self.unary.name.identifyingName == "Unary")
#expect(self.clientStreaming.name.identifyingName == "ClientStreaming")
#expect(self.serverStreaming.name.identifyingName == "ServerStreaming")
#expect(self.bidiStreaming.name.identifyingName == "BidirectionalStreaming")
@available(gRPCSwiftProtobuf 2.0, *)
func name() throws {
try #expect(self.unary.name.identifyingName == "Unary")
try #expect(self.clientStreaming.name.identifyingName == "ClientStreaming")
try #expect(self.serverStreaming.name.identifyingName == "ServerStreaming")
try #expect(self.bidiStreaming.name.identifyingName == "BidirectionalStreaming")
}
@Test("Input")
func input() {
#expect(self.unary.inputType == "Test_TestInput")
#expect(!self.unary.isInputStreaming)
@available(gRPCSwiftProtobuf 2.0, *)
func input() throws {
#expect(try self.unary.inputType == "Test_TestInput")
#expect(try !self.unary.isInputStreaming)
#expect(self.clientStreaming.inputType == "Test_TestInput")
#expect(self.clientStreaming.isInputStreaming)
#expect(try self.clientStreaming.inputType == "Test_TestInput")
#expect(try self.clientStreaming.isInputStreaming)
#expect(self.serverStreaming.inputType == "Test_TestInput")
#expect(!self.serverStreaming.isInputStreaming)
#expect(try self.serverStreaming.inputType == "Test_TestInput")
#expect(try !self.serverStreaming.isInputStreaming)
#expect(self.bidiStreaming.inputType == "Test_TestInput")
#expect(self.bidiStreaming.isInputStreaming)
#expect(try self.bidiStreaming.inputType == "Test_TestInput")
#expect(try self.bidiStreaming.isInputStreaming)
}
@Test("Output")
func output() {
#expect(self.unary.outputType == "Test_TestOutput")
#expect(!self.unary.isOutputStreaming)
@available(gRPCSwiftProtobuf 2.0, *)
func output() throws {
#expect(try self.unary.outputType == "Test_TestOutput")
#expect(try !self.unary.isOutputStreaming)
#expect(self.clientStreaming.outputType == "Test_TestOutput")
#expect(!self.clientStreaming.isOutputStreaming)
#expect(try self.clientStreaming.outputType == "Test_TestOutput")
#expect(try !self.clientStreaming.isOutputStreaming)
#expect(self.serverStreaming.outputType == "Test_TestOutput")
#expect(self.serverStreaming.isOutputStreaming)
#expect(try self.serverStreaming.outputType == "Test_TestOutput")
#expect(try self.serverStreaming.isOutputStreaming)
#expect(self.bidiStreaming.outputType == "Test_TestOutput")
#expect(self.bidiStreaming.isOutputStreaming)
#expect(try self.bidiStreaming.outputType == "Test_TestOutput")
#expect(try self.bidiStreaming.isOutputStreaming)
}
}
}
@ -153,51 +175,58 @@ struct ProtobufCodeGenParserTests {
static let descriptorSetName = "foo-service"
static let fileDescriptorName = "foo-service"
let codeGen: CodeGenerationRequest
init() throws {
let descriptor = try #require(try Self.fileDescriptor)
self.codeGen = try parseDescriptor(descriptor)
@available(gRPCSwiftProtobuf 2.0, *)
var codeGen: CodeGenerationRequest {
get throws {
let descriptor = try Self.fileDescriptor
return try parseDescriptor(descriptor)
}
}
@Test("Name")
func name() {
#expect(self.codeGen.fileName == "foo-service.proto")
@available(gRPCSwiftProtobuf 2.0, *)
func name() throws {
#expect(try self.codeGen.fileName == "foo-service.proto")
}
@Test("Dependencies")
func dependencies() {
@available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
let expected: [GRPCCodeGen.Dependency] = [
.init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import
]
#expect(self.codeGen.dependencies == expected)
#expect(try self.codeGen.dependencies == expected)
}
@Test("Service1")
@available(gRPCSwiftProtobuf 2.0, *)
func service1() throws {
let service = self.codeGen.services[0]
let service = try self.codeGen.services[0]
#expect(service.name.identifyingName == "foo.FooService1")
#expect(service.methods.count == 1)
}
@Test("Service1.Method")
@available(gRPCSwiftProtobuf 2.0, *)
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.inputType == "Foo_FooInput")
#expect(method.outputType == "Foo_FooOutput")
}
@Test("Service2")
@available(gRPCSwiftProtobuf 2.0, *)
func service2() throws {
let service = self.codeGen.services[1]
let service = try self.codeGen.services[1]
#expect(service.name.identifyingName == "foo.FooService2")
#expect(service.methods.count == 1)
}
@Test("Service2.Method")
@available(gRPCSwiftProtobuf 2.0, *)
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.inputType == "Foo_FooInput")
#expect(method.outputType == "Foo_FooOutput")
@ -209,18 +238,14 @@ struct ProtobufCodeGenParserTests {
static var descriptorSetName: 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")
func serviceName() {
#expect(self.service.name.identifyingName == "BarService")
@available(gRPCSwiftProtobuf 2.0, *)
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 fileDescriptorName = "wkt-service"
let codeGen: CodeGenerationRequest
init() throws {
let descriptor = try #require(try Self.fileDescriptor)
self.codeGen = try parseDescriptor(descriptor)
}
@Test("Dependencies")
func dependencies() {
@available(gRPCSwiftProtobuf 2.0, *)
func dependencies() throws {
let descriptor = try Self.fileDescriptor
let codeGen = try parseDescriptor(descriptor)
let expected: [Dependency] = [
Dependency(module: "GRPCProtobuf", 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 fileDescriptorName = "test-service"
@Test("Generate", arguments: [CodeGenerator.Config.AccessLevel.internal])
func generate(accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel) throws {
let generator = ProtobufCodeGenerator(
config: CodeGenerator.Config(
accessLevel: accessLevel,
accessLevelOnImports: false,
client: true,
server: true,
indentation: 2
)
)
enum Availability {
case `default`
case fooOS
var override: [(String, String)] {
switch self {
case .default:
return []
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
switch accessLevel {
@ -49,10 +75,13 @@ struct ProtobufCodeGeneratorTests {
fatalError()
}
let expectedAvailability = availability.expected
let generated = try generator.generateCode(
fileDescriptor: Self.fileDescriptor,
protoFileModuleMappings: ProtoFileToModuleMappings(),
extraModuleImports: []
extraModuleImports: [],
availabilityOverrides: availability.override
)
let expected = """
@ -60,6 +89,7 @@ struct ProtobufCodeGeneratorTests {
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: test-service.proto
@ -69,10 +99,12 @@ struct ProtobufCodeGeneratorTests {
import GRPCCore
import GRPCProtobuf
import SwiftProtobuf
// MARK: - test.TestService
/// Namespace containing generated types for the "test.TestService" service.
@available(\(expectedAvailability), *)
\(access) enum Test_TestService {
/// Service descriptor for the "test.TestService" service.
\(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService")
@ -136,6 +168,7 @@ struct ProtobufCodeGeneratorTests {
}
}
@available(\(expectedAvailability), *)
extension GRPCCore.ServiceDescriptor {
/// Service descriptor for the "test.TestService" service.
\(access) static let test_TestService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService")
@ -143,6 +176,7 @@ struct ProtobufCodeGeneratorTests {
// MARK: test.TestService (server)
@available(\(expectedAvailability), *)
extension Test_TestService {
/// Streaming variant of the service protocol for the "test.TestService" service.
///
@ -404,6 +438,7 @@ struct ProtobufCodeGeneratorTests {
}
// Default implementation of 'registerMethods(with:)'.
@available(\(expectedAvailability), *)
extension Test_TestService.StreamingServiceProtocol {
\(access) func registerMethods<Transport>(with router: inout GRPCCore.RPCRouter<Transport>) where Transport: GRPCCore.ServerTransport {
router.registerHandler(
@ -454,6 +489,7 @@ struct ProtobufCodeGeneratorTests {
}
// Default implementation of streaming methods from 'StreamingServiceProtocol'.
@available(\(expectedAvailability), *)
extension Test_TestService.ServiceProtocol {
\(access) func unary(
request: GRPCCore.StreamingServerRequest<Test_TestInput>,
@ -490,6 +526,7 @@ struct ProtobufCodeGeneratorTests {
}
// Default implementation of methods from 'ServiceProtocol'.
@available(\(expectedAvailability), *)
extension Test_TestService.SimpleServiceProtocol {
\(access) func unary(
request: GRPCCore.ServerRequest<Test_TestInput>,
@ -554,6 +591,7 @@ struct ProtobufCodeGeneratorTests {
// MARK: test.TestService (client)
@available(\(expectedAvailability), *)
extension Test_TestService {
/// Generated client protocol for the "test.TestService" service.
///
@ -812,6 +850,7 @@ struct ProtobufCodeGeneratorTests {
}
// Helpers providing default arguments to 'ClientProtocol' methods.
@available(\(expectedAvailability), *)
extension Test_TestService.ClientProtocol {
/// Call the "Unary" method.
///
@ -927,6 +966,7 @@ struct ProtobufCodeGeneratorTests {
}
// Helpers providing sugared APIs for 'ClientProtocol' methods.
@available(\(expectedAvailability), *)
extension Test_TestService.ClientProtocol {
/// Call the "Unary" method.
///
@ -1062,6 +1102,36 @@ struct ProtobufCodeGeneratorTests {
#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)")
@ -1070,16 +1140,13 @@ struct ProtobufCodeGeneratorTests {
static let fileDescriptorName = "foo-messages"
@Test("Generate")
@available(gRPCSwiftProtobuf 2.0, *)
func generate() throws {
let generator = ProtobufCodeGenerator(
config: CodeGenerator.Config(
accessLevel: .public,
accessLevelOnImports: false,
client: true,
server: true,
indentation: 2
)
)
var config: ProtobufCodeGenerator.Config = .defaults
config.accessLevel = .public
config.indentation = 2
let generator = ProtobufCodeGenerator(config: config)
let generated = try generator.generateCode(
fileDescriptor: Self.fileDescriptor,
@ -1092,6 +1159,7 @@ struct ProtobufCodeGeneratorTests {
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
// Source: foo-messages.proto

View File

@ -63,11 +63,12 @@ private func loadDescriptorSet(
)
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)
return DescriptorSet(proto: descriptorSet)
}
@available(gRPCSwiftProtobuf 2.0, *)
func parseDescriptor(
_ descriptor: FileDescriptor,
extraModuleImports: [String] = [],
@ -76,7 +77,8 @@ func parseDescriptor(
let parser = ProtobufCodeGenParser(
protoFileModuleMappings: .init(),
extraModuleImports: extraModuleImports,
accessLevel: accessLevel
accessLevel: accessLevel,
moduleNames: .defaults
)
return try parser.parse(descriptor: descriptor)
}

View File

@ -40,6 +40,7 @@ struct DetailedErrorTests {
(["Help", "Help", "Help"], [.help(.testValue), .help(.testValue), .help(.testValue)]),
] as [([String], [ErrorDetails])]
)
@available(gRPCSwiftProtobuf 2.0, *)
func rpcStatus(details: [String], expected: [ErrorDetails]) async throws {
let inProcess = InProcessTransport()
try await withGRPCServer(transport: inProcess.server, services: [ErrorThrowingService()]) { _ in
@ -95,11 +96,27 @@ struct DetailedErrorTests {
(.localizedMessage(.testValue), #"LocalizedMessage(locale: "l", message: "m")"#),
] as [(ErrorDetails, String)]
)
@available(gRPCSwiftProtobuf 2.0, *)
func errorInfoDescription(_ details: ErrorDetails, expected: String) {
#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 {
func throwError(
request: ThrowInput,
@ -170,14 +187,17 @@ private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ErrorInfo {
fileprivate static let testValue = Self(reason: "r", domain: "d", metadata: ["k": "v"])
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.RetryInfo {
fileprivate static let testValue = Self(delay: .seconds(1))
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.DebugInfo {
fileprivate static let testValue = Self(
stack: ["foo.foo()", "foo.bar()"],
@ -185,6 +205,7 @@ extension ErrorDetails.DebugInfo {
)
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.QuotaFailure {
fileprivate static let testValue = Self(
violations: [
@ -193,6 +214,7 @@ extension ErrorDetails.QuotaFailure {
)
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.PreconditionFailure {
fileprivate static let testValue = Self(
violations: [
@ -201,6 +223,7 @@ extension ErrorDetails.PreconditionFailure {
)
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.BadRequest {
fileprivate static let testValue = Self(
violations: [
@ -209,14 +232,17 @@ extension ErrorDetails.BadRequest {
)
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.RequestInfo {
fileprivate static let testValue = Self(requestID: "id", servingData: "d")
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.ResourceInfo {
fileprivate static let testValue = Self(type: "t", name: "n", errorDescription: "d")
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.Help {
fileprivate static let testValue = Self(
links: [
@ -225,6 +251,7 @@ extension ErrorDetails.Help {
)
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ErrorDetails.LocalizedMessage {
fileprivate static let testValue = Self(locale: "l", message: "m")
}

View File

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

View File

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

View File

@ -21,12 +21,12 @@ protoc=$(which protoc)
# Checkout and build the plugins.
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.
bin_path=$(swift build --package-path "$root" --show-bin-path)
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.
# Parameters:
@ -36,10 +36,10 @@ protoc_gen_grpc_swift="$bin_path/protoc-gen-grpc-swift"
# - $4 onwards: options to forward to the plugin
function generate_grpc {
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
args+=("--grpc-swift_opt=$option")
args+=("--grpc-swift-2_opt=$option")
done
invoke_protoc "${args[@]}" "$proto"
@ -94,7 +94,7 @@ function generate_error_service {
output="$root/Tests/GRPCProtobufTests/Errors/Generated"
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 ------------------------------------------------------------
@ -105,7 +105,9 @@ function generate_test_service_descriptor_set {
proto_path="$(dirname "$proto")"
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 {

View File

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

View File

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