Compare commits

...

37 Commits

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

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

Modifications:

- Add an article explaining how to achieve this

Result:

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

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

Modifications:

- Update the docs

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

- Regenerate generated code

Result:

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

Swift 6.1 emits a few new warnings from Swift Testing

Modifications:

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

Result:

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

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

Modifications:

- Update min required version
- Update expectations in tests

Result:

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

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

Motivation:

- Add the missing "s"

Result:

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

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

Modifications:

- Add missing Hashable conformance
- Fix incorrect doc

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

---------

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

---------

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Result:

- Generated code can include different module name imports.

---------

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

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

### Modifications:

* Add a new command plugin
* Refactor some errors

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

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

Flags:

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

More convenient code generation

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

---------

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

It is no longer required.

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

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

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

Modifications:

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

Result:

Better docs
2025-01-30 13:25:38 +00:00
Rick Newton-Rogers 219783a584
CI use 6.1 nightlies (#42)
CI use 6.1 nightlies now that Swift development is happening in the 6.1
branch
2025-01-30 10:24:08 +00:00
George Barnett 4bb3bea121
Bump dependency versions (#38) 2025-01-24 16:06:33 +00:00
George Barnett 44b2b6f65d
Move the generating stubs doc from grpc-swift (#37)
Motivation:

The doc on generating stubs should really live in this package, and not
the upstream.

Modifications:

- Bring it in tree
- Fix incorrect DocC struct which led to articles being listed twice.

Result:

Better docs
2025-01-24 14:17:10 +00:00
Rick Newton-Rogers 4153adaff5
Integration tests for the build plugin (#31)
### Motivation:

To protect against regressions in common use-cases of the
grpc-swift-protobuf build plugin.

### Modifications:

Add test cases which make use of the build plugin as a dependency and
ensure that they can compile and use the generated code
* top level config file
* peer config file
* separate service message protos
* cross directory imports
* two definitions
* nested definitions

The new tests are run as part of CI on PRs

### Result:

More CI.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2025-01-24 13:04:06 +00:00
George Barnett 7695d98c4d
Align on config name for use import access level (#35)
Motivation:

When the plugin was done we went through a few iterations of the shape
of config and settled on `accessLevelOnImports` vs.
`useAccessLevelOnImports` but that change wasn't made.

Modifications:

- Update config name.

Result:

API is what we planned it to be.
2025-01-24 10:41:41 +00:00
George Barnett 7d129cecc3
Add an article explaining how to install `protoc` (#36)
Motivation:

The official instructions for installing `protoc` don't include
instructions for using a package manager. A number of our docs refernce
having `protoc` installed as a prerequisite, it'd be useful to have docs
on how to install using a package manager.

Modifications:

- Add a doc about installing protoc

Result:

More docs
2025-01-24 10:03:13 +00:00
George Barnett c991945c2b
Fix deprecations from codegen changes (#34)
Motivation:

We renamed a few types/properties in the code gen module and deprecated
the old names.

Modifications:

- Use new APIs

Result:

No warnings
2025-01-23 10:47:45 +00:00
George Barnett 05334bbf44
Fix warnings about unused value in plugin error (#33)
Motivation:

The plugin error case for an incompatible target has an associated value
which is currently ignored.

Modifications:

- Use the value

Result:

Fewer warnings
2025-01-22 14:02:10 +00:00
Rick Newton-Rogers 0e6d93b8aa
Remove percent-encoded chars in build plugin paths (#32)
### Motivation:

Xcode provides paths to executables as strings referencing environment
variables and then passes those strings to the build plugin as URLs.
This meant that when we converted these URLs to strings they
percent-encoded some characters and were no longer valid paths e.g.

```
/${BUILD_DIR}/${CONFIGURATION}/protoc-gen-swift
```
became
```
/$%7BBUILD_DIR%7D/$%7BCONFIGURATION%7D/protoc-gen-swift
```

### Modifications:

Have our utility function for accessing absolute paths as strings strip
percent-encoding.

### Result:

The build plugin works in Xcode
2025-01-21 17:48:40 +00:00
Rick Newton-Rogers b036851fb8
Code generation build plugin (#28)
## Overview
New build plugin to generate gRPC services and protobuf messages

The SwiftPM build plugin will locate protobuf files in the `Sources`
directory (with the extension `.proto`) and attempt to invoke both the
`protoc-gen-swift` and `protoc-gen-grpc-swift` `protoc` plugins on them
to automatically generate Swift source. Behavior can be modified by
specifying one or more configuration files.

* For a given protobuf definition file the tool will search for
configuration files in the same and all parent directories and will use
the file lowest in the hierarchy.
* Most configuration if not specified will use the protoc plugin's own
defaults.
2025-01-21 14:08:51 +00:00
63 changed files with 3300 additions and 352 deletions

View File

@ -13,6 +13,37 @@ 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_0_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
runs-on: ubuntu-latest
outputs:
plugin-tests-matrix: '${{ steps.generate-matrix.outputs.plugin-tests-matrix }}'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- id: generate-matrix
run: echo "plugin-tests-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
env:
MATRIX_LINUX_5_9_ENABLED: false
MATRIX_LINUX_5_10_ENABLED: false
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:
name: Plugin tests
needs: construct-plugin-tests-matrix
uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
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,36 @@ 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_0_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
runs-on: ubuntu-latest
outputs:
plugin-tests-matrix: '${{ steps.generate-matrix.outputs.plugin-tests-matrix }}'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- id: generate-matrix
run: echo "plugin-tests-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
env:
MATRIX_LINUX_5_9_ENABLED: false
MATRIX_LINUX_5_10_ENABLED: false
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:
name: Plugin tests
needs: construct-plugin-tests-matrix
uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
with:
name: "Plugin tests"
matrix_string: '${{ needs.construct-plugin-tests-matrix.outputs.plugin-tests-matrix }}'
cxx-interop:
name: Cxx interop
@ -32,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

@ -36,6 +36,7 @@ dev/git.commit.template
dev/version-bump.commit.template
dev/protos/local/*
dev/protos/upstream/*
IntegrationTests/PluginTests/**/*.proto
.unacceptablelanguageignore
LICENSE
**/*.swift

View File

@ -0,0 +1,11 @@
{
"generatedSource": {
"accessLevel": "internal"
},
"protoc": {
"importPaths": [
".",
"../directory_1"
]
}
}

View File

@ -0,0 +1,5 @@
{
"generatedSource": {
"accessLevel": "internal"
}
}

View File

@ -0,0 +1,6 @@
{
"generatedSource": {
"accessLevel": "public"
}
}

View File

@ -0,0 +1,7 @@
// Leading trivia.
syntax = "proto3";
package foo;
message FooInput {}
message FooOutput {}

View File

@ -0,0 +1,13 @@
syntax = "proto3";
import "Foo/foo-messages.proto";
package foo;
service FooService1 {
rpc Foo (FooInput) returns (FooOutput) {}
}
service FooService2 {
rpc Foo (FooInput) returns (FooOutput) {}
}

View File

@ -0,0 +1,37 @@
// Copyright 2015, 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.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@ -0,0 +1,31 @@
// Copyright 2015, 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.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@ -0,0 +1,29 @@
// Copyright 2015, 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.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
import "Messages.proto";
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

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,74 @@
/*
* 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 GRPCCore
import GRPCInProcessTransport
import GRPCProtobuf
@main
struct PluginAdopter {
static func main() async throws {
let inProcess = InProcessTransport()
try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in
try await withGRPCClient(transport: inProcess.client) { client in
try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client))
}
}
try await withGRPCServer(transport: inProcess.server, services: [FooService1()]) { server in
try await withGRPCClient(transport: inProcess.client) { client in
try await Self.doRPC(Foo_FooService1.Client(wrapping: client))
}
}
}
static func doRPC<Transport>(_ greeter: Helloworld_Greeter.Client<Transport>) async throws {
do {
let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" })
print("Reply: \(reply.message)")
} catch {
print("Error: \(error)")
}
}
static func doRPC<Transport>(_ fooService1: Foo_FooService1.Client<Transport>) async throws {
do {
let reply = try await fooService1.foo(.with { _ in () })
print("Reply: \(reply.hashValue)")
} catch {
print("Error: \(error)")
}
}
}
struct Greeter: Helloworld_Greeter.SimpleServiceProtocol {
func sayHello(
request: Helloworld_HelloRequest,
context: ServerContext
) async throws -> Helloworld_HelloReply {
return .with { reply in
reply.message = "Hello, world!"
}
}
}
struct FooService1: Foo_FooService1.SimpleServiceProtocol {
func foo(request: Foo_FooInput, context: GRPCCore.ServerContext) async throws -> Foo_FooOutput {
return .with { _ in
()
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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 GRPCCore
import GRPCInProcessTransport
import GRPCProtobuf
@main
struct PluginAdopter {
static func main() async throws {
let inProcess = InProcessTransport()
try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in
try await withGRPCClient(transport: inProcess.client) { client in
try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client))
}
}
}
static func doRPC<Transport>(_ greeter: Helloworld_Greeter.Client<Transport>) async throws {
do {
let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" })
print("Reply: \(reply.message)")
} catch {
print("Error: \(error)")
}
}
}
struct Greeter: Helloworld_Greeter.SimpleServiceProtocol {
func sayHello(
request: Helloworld_HelloRequest,
context: ServerContext
) async throws -> Helloworld_HelloReply {
return .with { reply in
reply.message = "Hello, world!"
}
}
}

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

@ -0,0 +1,49 @@
// swift-tools-version: 6.0
/*
* 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 PackageDescription
let package = Package(
name: "grpc-adopter",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v11),
.visionOS(.v2),
],
dependencies: [
// Dependency on grpc-swift-protobuf to be added by setup-plugin-tests.sh script
.package(
url: "https://github.com/grpc/grpc-swift-2.git",
from: "2.0.0"
)
],
targets: [
.executableTarget(
name: "grpc-adopter",
dependencies: [
.product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift-2"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
],
plugins: [
.plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf")
]
)
]
)

View File

@ -23,15 +23,23 @@ 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",
exact: "2.0.0-beta.3"
url: "https://github.com/grpc/grpc-swift-2.git",
from: "2.0.0"
),
.package(
url: "https://github.com/apple/swift-protobuf.git",
@ -39,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"),
],
@ -63,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
@ -72,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
@ -92,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"),
],
@ -101,17 +125,87 @@ let targets: [Target] = [
],
swiftSettings: defaultSwiftSettings
),
// Code generator build plugin
.plugin(
name: "GRPCProtobufGenerator",
capability: .buildTool(),
dependencies: [
.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

@ -0,0 +1,210 @@
/*
* 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
/// The config of the build plugin.
struct BuildPluginConfig: Codable {
/// Config defining which components should be considered when generating source.
struct Generate {
/// Whether server code is generated.
///
/// Defaults to `true`.
var servers: Bool
/// Whether client code is generated.
///
/// Defaults to `true`.
var clients: Bool
/// Whether message code is generated.
///
/// Defaults to `true`.
var messages: Bool
static let defaults = Self(
servers: true,
clients: true,
messages: true
)
private init(servers: Bool, clients: Bool, messages: Bool) {
self.servers = servers
self.clients = clients
self.messages = messages
}
}
/// Config relating to the generated code itself.
struct GeneratedSource {
/// The visibility of the generated files.
///
/// Defaults to `Internal`.
var accessLevel: GenerationConfig.AccessLevel
/// Whether imports should have explicit access levels.
///
/// Defaults to `false`.
var accessLevelOnImports: Bool
static let defaults = Self(
accessLevel: .internal,
accessLevelOnImports: false
)
private init(accessLevel: GenerationConfig.AccessLevel, accessLevelOnImports: Bool) {
self.accessLevel = accessLevel
self.accessLevelOnImports = accessLevelOnImports
}
}
/// Config relating to the protoc invocation.
struct Protoc {
/// Specify the directory in which to search for imports.
///
/// Paths are relative to the location of the specifying config file.
/// Build plugins only have access to files within the target's source directory.
/// May be specified multiple times; directories will be searched in order.
/// The target source directory is always appended
/// to the import paths.
var importPaths: [String]
/// The path to the `protoc` executable binary.
///
/// If this is not set, Swift Package Manager will try to find the tool itself.
var executablePath: String?
static let defaults = Self(
importPaths: [],
executablePath: nil
)
private init(importPaths: [String], executablePath: String?) {
self.importPaths = importPaths
self.executablePath = executablePath
}
}
/// Config defining which components should be considered when generating source.
var generate: Generate
/// Config relating to the nature of the generated code.
var generatedSource: GeneratedSource
/// Config relating to the protoc invocation.
var protoc: Protoc
static let defaults = Self(
generate: Generate.defaults,
generatedSource: GeneratedSource.defaults,
protoc: Protoc.defaults
)
private init(generate: Generate, generatedSource: GeneratedSource, protoc: Protoc) {
self.generate = generate
self.generatedSource = generatedSource
self.protoc = protoc
}
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case generate
case generatedSource
case protoc
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.generate =
try container.decodeIfPresent(Generate.self, forKey: .generate) ?? Self.defaults.generate
self.generatedSource =
try container.decodeIfPresent(GeneratedSource.self, forKey: .generatedSource)
?? Self.defaults.generatedSource
self.protoc =
try container.decodeIfPresent(Protoc.self, forKey: .protoc) ?? Self.defaults.protoc
}
}
extension BuildPluginConfig.Generate: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case servers
case clients
case messages
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.servers =
try container.decodeIfPresent(Bool.self, forKey: .servers) ?? Self.defaults.servers
self.clients =
try container.decodeIfPresent(Bool.self, forKey: .clients) ?? Self.defaults.clients
self.messages =
try container.decodeIfPresent(Bool.self, forKey: .messages) ?? Self.defaults.messages
}
}
extension BuildPluginConfig.GeneratedSource: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case accessLevel
case accessLevelOnImports
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.accessLevel =
try container.decodeIfPresent(GenerationConfig.AccessLevel.self, forKey: .accessLevel)
?? Self.defaults.accessLevel
self.accessLevelOnImports =
try container.decodeIfPresent(Bool.self, forKey: .accessLevelOnImports)
?? Self.defaults.accessLevelOnImports
}
}
extension BuildPluginConfig.Protoc: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case importPaths
case executablePath
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.importPaths =
try container.decodeIfPresent([String].self, forKey: .importPaths)
?? Self.defaults.importPaths
self.executablePath = try container.decodeIfPresent(String.self, forKey: .executablePath)
}
}
extension GenerationConfig {
init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) {
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
configFilePath.deletingLastPathComponent().absoluteStringNoScheme + "/" + relativePath
}
self.protocPath = buildPluginConfig.protoc.executablePath
self.outputPath = outputPath.absoluteStringNoScheme
}
}

View File

@ -0,0 +1,31 @@
/*
* 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 BuildPluginError: Error {
case incompatibleTarget(String)
case noConfigFilesFound
}
extension BuildPluginError: CustomStringConvertible {
var description: String {
switch self {
case .incompatibleTarget(let target):
"Build plugin applied to incompatible target (\(target))."
case .noConfigFilesFound:
"No config files found. The build plugin relies on the existence of one or more '\(configFileName)' files in the target source."
}
}
}

View File

@ -0,0 +1,305 @@
/*
* 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
// Entry-point when using Package manifest
extension GRPCProtobufGenerator: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
guard let swiftTarget = target as? SwiftSourceModuleTarget else {
throw BuildPluginError.incompatibleTarget(target.name)
}
let configFiles = swiftTarget.sourceFiles(withSuffix: configFileName).map { $0.url }
let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url }
return try createBuildCommands(
pluginWorkDirectory: context.pluginWorkDirectoryURL,
tool: context.tool,
inputFiles: inputFiles,
configFiles: configFiles,
targetName: target.name
)
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
// Entry-point when using Xcode projects
extension GRPCProtobufGenerator: XcodeBuildToolPlugin {
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
let configFiles = target.inputFiles.filter {
$0.url.lastPathComponent == configFileName
}.map { $0.url }
let inputFiles = target.inputFiles.filter { $0.url.lastPathComponent.hasSuffix(".proto") }.map {
$0.url
}
return try createBuildCommands(
pluginWorkDirectory: context.pluginWorkDirectoryURL,
tool: context.tool,
inputFiles: inputFiles,
configFiles: configFiles,
targetName: target.displayName
)
}
}
#endif
@main
struct GRPCProtobufGenerator {
/// Build plugin common code
func createBuildCommands(
pluginWorkDirectory: URL,
tool: (String) throws -> PluginContext.Tool,
inputFiles: [URL],
configFiles: [URL],
targetName: String
) throws -> [Command] {
let configs = try readConfigFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory)
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 BuildPluginError.noConfigFilesFound
}
let protocPath = try deriveProtocPath(using: config, tool: tool)
let protoDirectoryPaths: [String]
if config.importPaths.isEmpty {
protoDirectoryPaths = [configFilePath.deletingLastPathComponent().absoluteStringNoScheme]
} else {
protoDirectoryPaths = config.importPaths
}
// unless *explicitly* opted-out
if config.clients || config.servers {
let grpcCommand = try protocGenGRPCSwiftCommand(
inputFile: inputFile,
config: config,
baseDirectoryPath: configFilePath.deletingLastPathComponent(),
protoDirectoryPaths: protoDirectoryPaths,
protocPath: protocPath,
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
configFilePath: configFilePath
)
commands.append(grpcCommand)
}
// unless *explicitly* opted-out
if config.messages {
let protoCommand = try protocGenSwiftCommand(
inputFile: inputFile,
config: config,
baseDirectoryPath: configFilePath.deletingLastPathComponent(),
protoDirectoryPaths: protoDirectoryPaths,
protocPath: protocPath,
protocGenSwiftPath: protocGenSwiftPath,
configFilePath: configFilePath
)
commands.append(protoCommand)
}
}
return commands
}
}
/// Reads the config files at the supplied URLs into memory
/// - Parameter configFilePaths: URLs from which to load config
/// - Returns: A map of source URLs to loaded config
func readConfigFiles(
_ configFilePaths: [URL],
pluginWorkDirectory: URL
) throws -> [URL: GenerationConfig] {
var configs: [URL: GenerationConfig] = [:]
for configFilePath in configFilePaths {
let data = try Data(contentsOf: configFilePath)
let config = try JSONDecoder().decode(BuildPluginConfig.self, from: data)
// the output directory mandated by the plugin system
configs[configFilePath] = GenerationConfig(
buildPluginConfig: config,
configFilePath: configFilePath,
outputPath: pluginWorkDirectory
)
}
return configs
}
extension [URL: GenerationConfig] {
/// Finds the most relevant config file for a given proto file URL.
///
/// The most relevant config file is the lowest of config files which are either a sibling or a parent in the file heirarchy.
/// - Parameters:
/// - file: The path to the proto file to be matched.
/// - Returns: The path to the most precisely relevant config file if one is found and the config itself, otherwise `nil`.
func findApplicableConfig(for file: URL) -> (URL, GenerationConfig)? {
let filePathComponents = file.pathComponents
for endComponent in (0 ..< filePathComponents.count).reversed() {
for (configFilePath, config) in self {
if filePathComponents[..<endComponent]
== configFilePath.pathComponents[..<(configFilePath.pathComponents.count - 1)]
{
return (configFilePath, config)
}
}
}
return nil
}
}
/// 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-2`.
/// - configFilePath: The path to the config file in use.
/// - Returns: The command to invoke `protoc` with the `protoc-gen-grpc-swift-2` plugin.
func protocGenGRPCSwiftCommand(
inputFile: URL,
config: GenerationConfig,
baseDirectoryPath: URL,
protoDirectoryPaths: [String],
protocPath: URL,
protocGenGRPCSwiftPath: URL,
configFilePath: URL
) throws -> PackagePlugin.Command {
let outputPathURL = URL(fileURLWithPath: config.outputPath)
let outputFilePath = deriveOutputFilePath(
protoFile: inputFile,
baseDirectoryPath: baseDirectoryPath,
outputDirectory: outputPathURL,
outputExtension: "grpc.swift"
)
let arguments = constructProtocGenGRPCSwiftArguments(
config: config,
fileNaming: config.fileNaming,
inputFiles: [inputFile],
protoDirectoryPaths: protoDirectoryPaths,
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
outputDirectory: outputPathURL
)
return Command.buildCommand(
displayName: "Generating gRPC Swift files for \(inputFile.absoluteStringNoScheme)",
executable: protocPath,
arguments: arguments,
inputFiles: [
inputFile,
protocGenGRPCSwiftPath,
configFilePath,
],
outputFiles: [outputFilePath]
)
}
/// Construct the command to invoke `protoc` with the `protoc-gen-swift` 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`
/// - 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(
inputFile: URL,
config: GenerationConfig,
baseDirectoryPath: URL,
protoDirectoryPaths: [String],
protocPath: URL,
protocGenSwiftPath: URL,
configFilePath: URL
) throws -> PackagePlugin.Command {
let outputPathURL = URL(fileURLWithPath: config.outputPath)
let outputFilePath = deriveOutputFilePath(
protoFile: inputFile,
baseDirectoryPath: baseDirectoryPath,
outputDirectory: outputPathURL,
outputExtension: "pb.swift"
)
let arguments = constructProtocGenSwiftArguments(
config: config,
fileNaming: config.fileNaming,
inputFiles: [inputFile],
protoDirectoryPaths: protoDirectoryPaths,
protocGenSwiftPath: protocGenSwiftPath,
outputDirectory: outputPathURL
)
return Command.buildCommand(
displayName: "Generating Swift Protobuf files for \(inputFile.absoluteStringNoScheme)",
executable: protocPath,
arguments: arguments,
inputFiles: [
inputFile,
protocGenSwiftPath,
configFilePath,
],
outputFiles: [outputFilePath]
)
}
/// 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:
/// - 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(
protoFile: URL,
baseDirectoryPath: URL,
outputDirectory: URL,
outputExtension: String
) -> URL {
// 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 = protoFile.deletingLastPathComponent().pathComponents
for protoDirectoryPathComponent in baseDirectoryPath.pathComponents {
if relativePathComponents.first == protoDirectoryPathComponent {
relativePathComponents.removeFirst()
} else {
break
}
}
relativePathComponents.append(fileName)
let path = relativePathComponents.joined(separator: "_")
return outputDirectory.appending(path: path)
}

View File

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

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

@ -0,0 +1,100 @@
/*
* 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.
*/
/// The config used when generating code whether called from the build or command plugin.
struct GenerationConfig {
/// The access level (i.e. visibility) of the generated files.
enum AccessLevel: String {
/// The generated files should have `internal` access level.
case `internal` = "Internal"
/// The generated files should have `public` access level.
case `public` = "Public"
/// The generated files should have `package` access level.
case `package` = "Package"
}
/// The naming of output files with respect to the path of the source file.
///
/// 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`
/// - `DropPath`: `baz.grpc.swift`
enum FileNaming: String {
/// Replicate the input file path with the output file(s).
case fullPath = "FullPath"
/// Convert path directory delimiters to underscores.
case pathToUnderscores = "PathToUnderscores"
/// Generate output files using only the base name of the inout file, ignoring the path.
case dropPath = "DropPath"
}
/// The visibility of the generated files.
var accessLevel: AccessLevel
/// Whether server code is generated.
var servers: Bool
/// Whether client code is generated.
var clients: Bool
/// Whether message code is generated.
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.
var accessLevelOnImports: Bool
/// Specify the directory in which to search for imports.
///
/// May be specified multiple times; directories will be searched in order.
/// The target source directory is always appended to the import paths.
var importPaths: [String]
/// The path to the `protoc` binary.
///
/// If this is not set, Swift Package Manager will try to find the tool itself.
var protocPath: String?
/// The path into which the generated source files are created.
var outputPath: String
}
extension GenerationConfig.AccessLevel: Codable {
init?(rawValue: String) {
switch rawValue.lowercased() {
case "internal":
self = .internal
case "public":
self = .public
case "package":
self = .package
default:
return nil
}
}
}
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

@ -0,0 +1,132 @@
/*
* 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
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`.
/// - findTool: The context-supplied tool which is used to attempt to discover the path to a `protoc` binary.
/// - Returns: The path to the instance of `protoc` to be used.
func deriveProtocPath(
using config: GenerationConfig,
tool findTool: (String) throws -> PackagePlugin.PluginContext.Tool
) throws -> URL {
if let configuredProtocPath = config.protocPath {
return URL(fileURLWithPath: configuredProtocPath)
} else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] {
// The user set the env variable, so let's take that
return URL(fileURLWithPath: environmentPath)
} else {
// The user didn't set anything so let's try see if Swift Package Manager can find a binary for us
return try findTool("protoc").url
}
}
/// Construct the arguments to be passed to `protoc` when invoking the `protoc-gen-swift` `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.
/// - protocGenSwiftPath: The path to the `protoc-gen-swift` `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-swift` `protoc` plugin.
func constructProtocGenSwiftArguments(
config: GenerationConfig,
fileNaming: GenerationConfig.FileNaming?,
inputFiles: [URL],
protoDirectoryPaths: [String],
protocGenSwiftPath: URL,
outputDirectory: URL
) -> [String] {
var protocArgs = [
"--plugin=protoc-gen-swift=\(protocGenSwiftPath.absoluteStringNoScheme)",
"--swift_out=\(outputDirectory.absoluteStringNoScheme)",
]
for path in protoDirectoryPaths {
protocArgs.append("--proto_path=\(path)")
}
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 })
return protocArgs
}
/// 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-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-2` `protoc` plugin.
func constructProtocGenGRPCSwiftArguments(
config: GenerationConfig,
fileNaming: GenerationConfig.FileNaming?,
inputFiles: [URL],
protoDirectoryPaths: [String],
protocGenGRPCSwiftPath: URL,
outputDirectory: URL
) -> [String] {
var protocArgs = [
"--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath.absoluteStringNoScheme)",
"--grpc-swift_out=\(outputDirectory.absoluteStringNoScheme)",
]
for path in protoDirectoryPaths {
protocArgs.append("--proto_path=\(path)")
}
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 })
return protocArgs
}
extension URL {
/// Returns `URL.absoluteString` with the `file://` scheme prefix removed
///
/// Note: This method also removes percent-encoded UTF-8 characters
var absoluteStringNoScheme: String {
var absoluteString = self.absoluteString.removingPercentEncoding ?? self.absoluteString
absoluteString.trimPrefix("file://")
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

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

View File

@ -0,0 +1,178 @@
# Generating stubs
Learn how to generate stubs for gRPC Swift from a service defined using the Protocol Buffers IDL.
## Overview
If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're
unfamiliar with Protocol Buffers then you should get comfortable with the concepts before
continuing; the [Protocol Buffers website](https://protobuf.dev/) is a great place to start.
You can use the `protoc` plugin from the command line directly, or you can make use of a
[Swift Package Manager build plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md)
convenience which adds the stub generation to the Swift build graph.
You may use the build plugin either from the command line or from Xcode.
## Using the build plugin
The build plugin (`GRPCProtobufGenerator`) is a great choice for convenient dynamic code generation, however it does come with some limitations.
Because it generates the gRPC Swift stubs as part of the build it has the requirement that `protoc` must be available
at compile time. This requirement means it is not a good fit for library authors who do not have
direct control over this.
The build plugin detects `.proto` files in the source tree and invokes `protoc` once for each file
(caching results and performing the generation as necessary).
### Adoption
You must adopt Swift Package Manager build plugins on a per-target basis by modifying your package manifest
(`Package.swift` file). To do this, declare the grpc-swift-protobuf package as a dependency and add the plugin
to your desired targets.
For example, to make use of the plugin for generating gRPC Swift stubs as part of the
`echo-server` target:
```swift
targets: [
.executableTarget(
name: "echo-server",
dependencies: [
// ...
],
plugins: [
.plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf")
]
)
]
```
Once this is done you need to ensure that the `.proto` files to be used for generation
are included in the target's source directory and that you have defined at least one configuration file.
### Configuration
You must provide a configuration file in the directory which encloses all `.proto` files (in the same directory or a parent).
Configuration files, written in JSON, tell the build plugin about the options used for `protoc` invocations.
You must name the file `grpc-swift-proto-generator-config.json` and structure it in the following format:
```json
{
"generate": {
"clients": true,
"servers": true,
"messages": true
},
"generatedSource": {
"accessLevelOnImports": false,
"accessLevel": "internal"
},
"protoc": {
"executablePath": "/opt/homebrew/bin/protoc",
"importPaths": [
"../directory_1"
]
}
}
```
The options do not need to be specified and each have default values.
| Name | Possible Values | Default | Description |
|----------------------------------------|--------------------------------------------|--------------|-----------------------------------------------------|
| `generate.servers` | `true`, `false` | `true` | Generate server stubs |
| `generate.clients` | `true`, `false` | `true` | Generate client stubs |
| `generate.messages` | `true`, `false` | `true` | Generate message stubs |
| `generatedSource.accessLevelOnImports` | `true`, `false` | `false` | Whether imports should have explicit access levels |
| `generatedSource.accessLevel` | `"public"`, `"package"`, `"internal"` | `"internal"` | Access level for generated stubs |
| `protoc.executablePath` | N/A | `null`† | Path to the `protoc` executable |
| `protoc.importPaths` | N/A | `null`‡ | Import paths passed to `protoc` |
† The Swift Package Manager build plugin infrastructure will attempt to discover the executable's location if you don't provide one.
‡ 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-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
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-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-2_out=<DIRECTORY>` option:
```console
protoc --grpc-swift-2_out=. my-service.proto
```
> `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-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-2 --grpc-swift-2_out=. my-service.proto
```
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-2_opt=<OPTION_NAME>=<OPTION_VALUE> --grpc-swift-2_out=.
```
You can specify multiple options by passing the `--grpc-swift-2_opt` argument multiple times:
```console
protoc \
--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`, `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
output file will be generated:
- `FullPath`: `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`
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-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-2`
by building it directly:
```console
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
`swift build --show-bin-path`.

View File

@ -0,0 +1,59 @@
# Installing protoc
Learn how to install `protoc`, the Protocol Buffers compiler.
## Overview
The Protocol Buffers compiler is a command line tool for generating source code from `.proto`
files and is required to generate gRPC stubs and messages. You can learn more about it on the
[Protocol Buffers website](https://protobuf.dev/).
You can install `protoc` in a number of ways including:
1. Via a package manager,
2. By downloading the binary.
### Install via a package manager
Using a package manager is the easiest way to install `protoc`.
On macOS you can use [Homebrew](https://brew.sh):
```sh
brew install protobuf
```
On Ubuntu and Debian you can use `apt`:
```sh
apt update && apt install -y protobuf-compiler
```
On Fedora you can use `dnf`:
```sh
dnf install -y protobuf-compiler
```
### Installing a pre-built binary
If you're unable to use a package manager to install `protoc` then you may be able
to download a pre-built binary from the [Protocol Buffers GitHub
repository](https://github.com/protocolbuffers/protobuf).
First, find and download the appropriate binary for your system from the
[releases](https://github.com/protocolbuffers/protobuf/releases) page.
Next, unzip the artifact to a directory called `protoc`:
```sh
unzip /path/to/downloaded/protoc-{VERSION}-{OS}.zip -d protoc
```
Finally, move `protoc/bin/protoc` to somewhere in your `$PATH` such as `/usr/local/bin`:
```sh
mv protoc/bin/protoc /usr/local/bin
```
You can now remove the `protoc` directory.

View File

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

View File

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

View File

@ -4,16 +4,27 @@ A package integrating Swift Protobuf with gRPC Swift.
## Overview
This package provides two products:
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).
- `GRPCProtobufGenerator`, a Swift Package build plugin for generating stubs as part of the build
process.
## Topics
### Essentials
- <doc:Installing-protoc>
- <doc:Generating-stubs>
- <doc:API-stability-of-generated-code>
- <doc:Understanding-the-generated-code>
- <doc:Public-services-with-private-implementations>
### Serialization
- ``ProtobufSerializer``

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

@ -18,11 +18,12 @@ internal import SwiftProtobuf
package import SwiftProtobufPluginLibrary
package import struct GRPCCodeGen.CodeGenerationRequest
package import struct GRPCCodeGen.CodeGenerator
package import struct GRPCCodeGen.Dependency
package import struct GRPCCodeGen.MethodDescriptor
package import struct GRPCCodeGen.Name
package import struct GRPCCodeGen.MethodName
package import struct GRPCCodeGen.ServiceDescriptor
package import struct GRPCCodeGen.SourceGenerator
package import struct GRPCCodeGen.ServiceName
#if canImport(FoundationEssentials)
internal import struct FoundationEssentials.IndexPath
@ -31,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: SourceGenerator.Config.AccessLevel
let accessLevel: CodeGenerator.Config.AccessLevel
let moduleNames: ProtobufCodeGenerator.Config.ModuleNames
package init(
protoFileModuleMappings: ProtoFileToModuleMappings,
extraModuleImports: [String],
accessLevel: SourceGenerator.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 {
@ -61,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)
@ -69,12 +75,6 @@ package struct ProtobufCodeGenParser {
// https://github.com/grpc/grpc-swift
"""
let lookupSerializer: (String) -> String = { messageType in
"GRPCProtobuf.ProtobufSerializer<\(messageType)>()"
}
let lookupDeserializer: (String) -> String = { messageType in
"GRPCProtobuf.ProtobufDeserializer<\(messageType)>()"
}
let services = descriptor.services.map {
GRPCCodeGen.ServiceDescriptor(
@ -90,12 +90,17 @@ package struct ProtobufCodeGenParser {
leadingTrivia: header + leadingTrivia,
dependencies: self.codeDependencies(file: descriptor),
services: services,
lookupSerializer: lookupSerializer,
lookupDeserializer: lookupDeserializer
makeSerializerCodeSnippet: { messageType in
"\(self.moduleNames.grpcProtobuf).ProtobufSerializer<\(messageType)>()"
},
makeDeserializerCodeSnippet: { messageType in
"\(self.moduleNames.grpcProtobuf).ProtobufDeserializer<\(messageType)>()"
}
)
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension ProtobufCodeGenParser {
fileprivate func codeDependencies(file: FileDescriptor) -> [Dependency] {
guard file.services.count > 0 else {
@ -103,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.
//
@ -114,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
@ -134,6 +143,7 @@ extension ProtobufCodeGenParser {
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCodeGen.ServiceDescriptor {
fileprivate init(
descriptor: SwiftProtobufPluginLibrary.ServiceDescriptor,
@ -147,35 +157,31 @@ extension GRPCCodeGen.ServiceDescriptor {
protobufNamer: protobufNamer
)
}
let name = Name(
base: descriptor.name,
let typePrefix = protobufNamer.typePrefix(forFile: file)
let name = ServiceName(
identifyingName: descriptor.fullName,
// The service name from the '.proto' file is expected to be in upper camel case
generatedUpperCase: descriptor.name,
generatedLowerCase: CamelCaser.toLowerCamelCase(descriptor.name)
typeName: typePrefix + descriptor.name,
propertyName: protobufNamer.typePrefixProperty(file: file) + descriptor.name
)
// Packages that are based on the path of the '.proto' file usually
// contain dots. For example: "grpc.test".
let namespace = Name(
base: package,
generatedUpperCase: protobufNamer.formattedUpperCasePackage(file: file),
generatedLowerCase: protobufNamer.formattedLowerCasePackage(file: file)
)
let documentation = descriptor.protoSourceComments()
self.init(documentation: documentation, name: name, namespace: namespace, methods: methods)
self.init(documentation: documentation, name: name, methods: methods)
}
}
@available(gRPCSwiftProtobuf 2.0, *)
extension GRPCCodeGen.MethodDescriptor {
fileprivate init(
descriptor: SwiftProtobufPluginLibrary.MethodDescriptor,
protobufNamer: SwiftProtobufNamer
) {
let name = Name(
base: descriptor.name,
let name = MethodName(
identifyingName: descriptor.name,
// The method name from the '.proto' file is expected to be in upper camel case
generatedUpperCase: descriptor.name,
generatedLowerCase: CamelCaser.toLowerCamelCase(descriptor.name)
typeName: descriptor.name,
functionName: CamelCaser.toLowerCamelCase(descriptor.name)
)
let documentation = descriptor.protoSourceComments()
self.init(
@ -208,17 +214,19 @@ extension FileDescriptor {
}
extension SwiftProtobufNamer {
internal func formattedUpperCasePackage(file: FileDescriptor) -> String {
let unformattedPackage = self.typePrefix(forFile: file)
return unformattedPackage.trimTrailingUnderscores()
}
internal func formattedLowerCasePackage(file: FileDescriptor) -> String {
let upperCasePackage = self.formattedUpperCasePackage(file: file)
let lowerCaseComponents = upperCasePackage.split(separator: "_").map { component in
internal func typePrefixProperty(file: FileDescriptor) -> String {
let typePrefix = self.typePrefix(forFile: file)
let lowercased = typePrefix.split(separator: "_").map { component in
NamingUtils.toLowerCamelCase(String(component))
}
return lowerCaseComponents.joined(separator: "_")
let joined = lowercased.joined(separator: "_")
if typePrefix.hasSuffix("_"), !joined.hasSuffix("_") {
// Add the trailing "_" if it was dropped.
return joined + "_"
} else {
return joined
}
}
}

View File

@ -17,11 +17,12 @@
package import GRPCCodeGen
package import SwiftProtobufPluginLibrary
@available(gRPCSwiftProtobuf 2.0, *)
package struct ProtobufCodeGenerator {
internal var config: SourceGenerator.Config
internal var config: ProtobufCodeGenerator.Config
package init(
config: SourceGenerator.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 sourceGenerator = SourceGenerator(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 sourceGenerator.generate(codeGenerationRequest)
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,7 +26,8 @@ import Foundation
#endif
@main
final class GenerateGRPC: CodeGenerator {
@available(gRPCSwiftProtobuf 2.0, *)
final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator {
var version: String? {
Version.versionString
}
@ -55,37 +56,10 @@ final class GenerateGRPC: 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: CodeGenerator {
fileNamingOption: options.fileNaming
)
let config = SourceGenerator.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 SourceGenerator.Config {
init(options: GeneratorOptions) {
let accessLevel: SourceGenerator.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,99 +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.base == "TestService")
}
@Test("Namespace")
func namespace() {
#expect(self.service.namespace.base == "test")
@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.base == "Unary")
#expect(self.clientStreaming.name.base == "ClientStreaming")
#expect(self.serverStreaming.name.base == "ServerStreaming")
#expect(self.bidiStreaming.name.base == "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)
}
}
}
@ -158,54 +175,59 @@ 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]
#expect(service.name.base == "FooService1")
#expect(service.namespace.base == "foo")
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]
#expect(method.name.base == "Foo")
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]
#expect(service.name.base == "FooService2")
#expect(service.namespace.base == "foo")
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]
#expect(method.name.base == "Foo")
let method = try self.codeGen.services[1].methods[0]
#expect(method.name.identifyingName == "Foo")
#expect(method.inputType == "Foo_FooInput")
#expect(method.outputType == "Foo_FooOutput")
}
@ -216,23 +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.base == "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)
@Test("Service namespace")
func serviceNamespace() {
#expect(self.service.namespace.base == "")
#expect(service.name.identifyingName == "BarService")
}
}
@ -241,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: [SourceGenerator.Config.AccessLevel.internal, .public, .package])
func generate(accessLevel: SourceGenerator.Config.AccessLevel) throws {
let generator = ProtobufCodeGenerator(
config: SourceGenerator.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: SourceGenerator.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

@ -21,7 +21,7 @@ import SwiftProtobufPluginLibrary
import Testing
import struct GRPCCodeGen.CodeGenerationRequest
import struct GRPCCodeGen.SourceGenerator
import struct GRPCCodeGen.CodeGenerator
protocol UsesDescriptorSet {
static var descriptorSetName: String { get }
@ -63,20 +63,22 @@ 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] = [],
accessLevel: SourceGenerator.Config.AccessLevel = .internal
accessLevel: CodeGenerator.Config.AccessLevel = .internal
) throws -> CodeGenerationRequest {
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()

40
dev/execute-plugin-tests.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
## 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.
set -euo pipefail
log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
tests_directory="${PLUGIN_TESTS_DIRECTORY:=""}"
if [[ -z "$tests_directory" ]]; then
fatal "Tests parent directory must be specified."
fi
for dir in "$tests_directory"/test_*/ ; do
if [[ -f "$dir/Package.swift" ]]; then
plugin_test=$(basename "$dir")
log "Building '$plugin_test' plugin test"
if ! build_output=$(swift build --package-path "$dir" 2>&1); then
# Only print the build output on failure.
echo "$build_output"
fatal "Build failed for '$plugin_test'"
else
log "Build succeeded for '$plugin_test'"
fi
fi
done

33
dev/plugin-tests.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
## 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.
set -euo pipefail
log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
if [[ -n ${GITHUB_ACTIONS:=""} ]]; then
# we will have been piped to bash and won't know the location of the script
echo "Running in GitHub Actions"
source_directory="$(pwd)"
else
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source_directory="$(readlink -f "${here}/..")"
fi
tests_directory="${PLUGIN_TESTS_OUTPUT_DIRECTORY:=$(mktemp -d)}"
PLUGIN_TESTS_OUTPUT_DIRECTORY="$tests_directory" "${source_directory}/dev/setup-plugin-tests.sh"
PLUGIN_TESTS_DIRECTORY="$tests_directory" "${source_directory}/dev/execute-plugin-tests.sh"

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 {}

208
dev/setup-plugin-tests.sh Executable file
View File

@ -0,0 +1,208 @@
#!/bin/bash
## 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.
set -euo pipefail
log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
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}/Sources/Package.swift" "${scratch_directory}/"
cat >> "${package_manifest}" <<- EOM
package.dependencies.append(
.package(path: "$grpc_swift_protobuf_directory")
)
EOM
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]}"
}
function test_01_top_level_config_file {
# .
# ├── Package.swift
# └── Sources
# ├── HelloWorldAdopter.swift
# ├── Protos
# │ └── HelloWorld.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 "${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"
}
function test_02_peer_config_file {
# .
# ├── Package.swift
# └── Sources
# ├── HelloWorldAdopter.swift
# └── Protos
# ├── HelloWorld.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 "${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"
}
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