This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 0ebf1bab4 feat(compiler): add dart gRPC codegen (#3723)
0ebf1bab4 is described below
commit 0ebf1bab48180686f61e5e667b671c23c8090890
Author: Yash Agarwal <[email protected]>
AuthorDate: Sun Jun 21 18:23:49 2026 +0530
feat(compiler): add dart gRPC codegen (#3723)
## Why?
Dart users need gRPC support from FDL/proto/fbs schemas
## What does this PR do?
Adds DartServiceGeneratorMixin so foryc --grpc --dart_out=… emits a
<package>/<stem>_grpc.dart next to the messages file. Includes:
- <Service>Client over package:grpc's Client with one method per RPC,
covering unary and all three streaming modes (server-stream,
client-stream, bidi).
- abstract <Service>ServiceBase over Service with $addMethod
registrations and a _Pre shim per method.
- Top-level _serialize / _deserialize helpers routing through the schema
module's getFory(), with reference tracking on so payloads round-trip
with the Java/Python peers.
- Works from FDL, Protobuf, and FlatBuffers IDL service definitions.
- Class- and method-name collision detection.
- Opt-in dart analyze + dart format smoke test on the emitted file.
- A Java↔Dart gRPC interop test exercising all four modes in both
directions.
## Related issues
Part of #3266
Part of #3279
Closes #3266
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
- New _grpc.dart companion when --grpc --dart_out is used on a schema
with services.
## Benchmark
---------
Co-authored-by: chaokunyang <[email protected]>
---
.agents/languages/dart.md | 2 +
.github/workflows/ci.yml | 49 ++
compiler/fory_compiler/generators/dart.py | 11 +-
compiler/fory_compiler/generators/services/dart.py | 390 +++++++++++++
.../fory_compiler/tests/test_service_codegen.py | 634 +++++++++++++++++++++
docs/compiler/compiler-guide.md | 14 +-
docs/compiler/flatbuffers-idl.md | 17 +-
docs/compiler/generated-code.md | 41 ++
docs/compiler/index.md | 18 +-
docs/compiler/protobuf-idl.md | 41 +-
docs/compiler/schema-idl.md | 8 +-
docs/guide/dart/grpc-support.md | 292 ++++++++++
docs/guide/dart/index.md | 1 +
docs/guide/dart/troubleshooting.md | 17 +
integration_tests/grpc_tests/dart/.gitignore | 4 +
integration_tests/grpc_tests/dart/bin/interop.dart | 520 +++++++++++++++++
.../grpc_tests/{run_tests.sh => dart/pubspec.yaml} | 31 +-
integration_tests/grpc_tests/generate_grpc.py | 2 +
.../{RustGrpcTest.java => DartGrpcTest.java} | 29 +-
.../org/apache/fory/grpc_tests/GoGrpcTest.java | 16 +-
.../org/apache/fory/grpc_tests/GrpcTestBase.java | 197 +------
.../org/apache/fory/grpc_tests/KotlinGrpcTest.java | 22 +-
.../fory/grpc_tests/PythonAsyncGrpcTest.java | 38 +-
.../apache/fory/grpc_tests/PythonSyncGrpcTest.java | 41 +-
.../org/apache/fory/grpc_tests/RustGrpcTest.java | 26 +-
integration_tests/grpc_tests/run_tests.sh | 35 +-
26 files changed, 2228 insertions(+), 268 deletions(-)
diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md
index f257ece60..d4f6bebbb 100644
--- a/.agents/languages/dart.md
+++ b/.agents/languages/dart.md
@@ -26,6 +26,8 @@ Load this file when changing `dart/`.
- Generated struct serializers should use serializer-owned field descriptors
for runtime resolver decisions and emit direct field-specific write/read code
for static schemas. Do not route generated hot writes through generic
field-info value helpers such as `writeGeneratedStructFieldInfoValue`.
- Dart xlang or runtime ownership changes need local Dart package tests plus
the Java-driven `DartXlangTest`; package-only smoke tests are not enough.
- When claiming non-VM Dart support, prove a relevant non-VM compile path such
as `dart compile js` against active runtime or example code.
+- Generated Dart gRPC service companions (`<stem>_grpc.dart`) are
compiler-owned files that depend on the application-provided `grpc` package,
not `dart/packages/fory`. Keep gRPC dependencies out of the Fory Dart runtime
package.
+- Dart generated schema modules (`<Stem>ForyModule`) are the source-file
owners and own a ready `Fory` runtime: `getFory()` initializes a default
runtime and registers the schema's types on first use, so generated gRPC
companions never require a manual `install(...)`; `install(customFory)` stays
optional injection. Keep `getFory()` ready by construction, and do not
introduce package-derived aliases or duplicate serializer registration paths.
## Commands
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1a9ee4dd5..d618e65cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -855,6 +855,55 @@ jobs:
cd integration_tests/grpc_tests/java
mvn -T16 --no-transfer-progress -Dtest=KotlinGrpcTest test
+ grpc_java_dart_tests:
+ name: Java/Dart gRPC Tests
+ needs: changes
+ if: needs.changes.outputs.grpc_tests == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: 21
+ distribution: "temurin"
+ - name: Set up Python 3.11
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.11
+ cache: "pip"
+ - name: Set up Dart
+ uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c #
v1.7.1
+ with:
+ sdk: stable
+ - name: Cache Maven local repository
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+ - name: Install Java artifacts for gRPC tests
+ run: |
+ cd java
+ mvn -T16 --no-transfer-progress clean install -DskipTests
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true
+ - name: Generate gRPC test sources
+ run: python integration_tests/grpc_tests/generate_grpc.py
+ - name: Build Dart gRPC peer
+ run: |
+ cd integration_tests/grpc_tests/dart
+ dart pub get
+ dart run build_runner build
+ - name: Analyze and format-check generated Dart gRPC companions
+ run: |
+ cd integration_tests/grpc_tests/dart
+ dart analyze bin lib/generated/*/*_grpc.dart
+ dart format --output=none --set-exit-if-changed bin
lib/generated/*/*_grpc.dart
+ - name: Run Java/Dart gRPC Tests
+ run: |
+ cd integration_tests/grpc_tests/java
+ mvn -T16 --no-transfer-progress -Dtest=DartGrpcTest test
+
javascript:
name: JavaScript CI
needs: changes
diff --git a/compiler/fory_compiler/generators/dart.py
b/compiler/fory_compiler/generators/dart.py
index 557cd19b9..620dc28e7 100644
--- a/compiler/fory_compiler/generators/dart.py
+++ b/compiler/fory_compiler/generators/dart.py
@@ -23,6 +23,7 @@ from typing import Dict, List, Optional, Set, Tuple
from fory_compiler.frontend.utils import parse_idl_file
from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.generators.services.dart import DartServiceGeneratorMixin
from fory_compiler.ir.ast import (
ArrayType,
Enum,
@@ -40,7 +41,7 @@ from fory_compiler.ir.ast import (
from fory_compiler.ir.types import PrimitiveKind
-class DartGenerator(BaseGenerator):
+class DartGenerator(DartServiceGeneratorMixin, BaseGenerator):
language_name = "dart"
file_extension = ".dart"
@@ -245,6 +246,8 @@ class DartGenerator(BaseGenerator):
names: Set[str] = set()
for defs in (self.schema.enums, self.schema.unions,
self.schema.messages):
for item in defs:
+ if self.is_imported_type(item):
+ continue
names.add(self.safe_type_identifier(self.to_pascal_case(item.name)))
return names
@@ -1291,8 +1294,10 @@ class DartGenerator(BaseGenerator):
lines.extend(
[
f"{self.indent_str * (indent + 1)}static Fory getFory() {{",
- f"{self.indent_str * (indent + 2)}final fory = _fory;",
- f"{self.indent_str * (indent + 2)}if (fory == null) throw
StateError('Call {self.module_type_name()}.install(...) before using generated
helpers.');",
+ f"{self.indent_str * (indent + 2)}final existing = _fory;",
+ f"{self.indent_str * (indent + 2)}if (existing != null) return
existing;",
+ f"{self.indent_str * (indent + 2)}final fory = Fory();",
+ f"{self.indent_str * (indent + 2)}install(fory);",
f"{self.indent_str * (indent + 2)}return fory;",
f"{self.indent_str * (indent + 1)}}}",
"",
diff --git a/compiler/fory_compiler/generators/services/dart.py
b/compiler/fory_compiler/generators/services/dart.py
new file mode 100644
index 000000000..e3da08d0f
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/dart.py
@@ -0,0 +1,390 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""Dart gRPC service generator helpers."""
+
+from pathlib import Path
+from typing import Dict, List, Set, Tuple
+
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.ir.ast import RpcMethod, Service
+
+
+class DartServiceGeneratorMixin:
+ """Generates Dart gRPC service companions for all four RPC modes."""
+
+ _DART_RESERVED_METHOD_NAMES = frozenset(
+ {"toString", "hashCode", "noSuchMethod", "runtimeType"}
+ )
+ _DART_GRPC_SUPPORT_NAMES = frozenset(
+ {
+ "CallOptions",
+ "Client",
+ "ClientMethod",
+ "Future",
+ "List",
+ "ResponseFuture",
+ "ResponseStream",
+ "Service",
+ "ServiceCall",
+ "ServiceMethod",
+ "Stream",
+ "String",
+ "T",
+ "Uint8List",
+ "bytes",
+ "call",
+ "int",
+ "options",
+ "request",
+ "u8",
+ "value",
+ }
+ )
+
+ def generate_services(self) -> List[GeneratedFile]:
+ local_services = [
+ service
+ for service in self.schema.services
+ if not self.is_imported_type(service)
+ ]
+ if not local_services:
+ return []
+ self.check_dart_grpc_service_collisions(local_services)
+ self.check_dart_grpc_method_collisions(local_services)
+ self.check_dart_grpc_reserved_method_names(local_services)
+ return [self.generate_grpc_module(local_services)]
+
+ def check_dart_grpc_service_collisions(self, services: List[Service]) ->
None:
+ generated_names = set(self._top_level_names())
+ service_names = set()
+ for service in services:
+ for emitted in (f"{service.name}Client",
f"{service.name}ServiceBase"):
+ if emitted in generated_names or emitted in service_names:
+ raise ValueError(
+ f"Dart gRPC class {emitted} conflicts with a generated
"
+ "type or another service; rename the service or type"
+ )
+ service_names.add(emitted)
+
+ def check_dart_grpc_method_collisions(self, services: List[Service]) ->
None:
+ for service in services:
+ seen = {}
+ for method in service.methods:
+ emitted = self.dart_grpc_method_name(method)
+ if emitted in seen:
+ raise ValueError(
+ f"Dart gRPC method name collision in service
{service.name}: "
+ f"{seen[emitted]} and {method.name} both generate
{emitted}"
+ )
+ seen[emitted] = method.name
+
+ def check_dart_grpc_reserved_method_names(self, services: List[Service])
-> None:
+ offenders = []
+ for service in services:
+ for method in service.methods:
+ emitted = self.dart_grpc_method_name(method)
+ if emitted in self._DART_RESERVED_METHOD_NAMES:
+ offenders.append(f"{service.name}.{method.name} ->
{emitted}")
+ if offenders:
+ joined = "\n - " + "\n - ".join(offenders)
+ raise ValueError(
+ "Dart gRPC method name collides with an inherited Dart member "
+ "(Object/Client/Service) and would produce an invalid
override; "
+ "rename the RPC method:" + joined
+ )
+
+ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile:
+ """Emit a grpc-dart companion module for schema services."""
+ models_output = Path(
+ self.output_file_path()
+ ) # e.g. "demo/greeter/greeter.dart"
+ models_stem = models_output.stem # e.g. "greeter"
+ grpc_path = str(models_output.with_name(f"{models_stem}_grpc.dart"))
+
+ self._grpc_model_alias = "_models"
+ self._grpc_payload_imports: Dict[str, Tuple[str, str]] = {}
+ self._grpc_used_import_aliases =
self._dart_grpc_reserved_import_aliases(
+ services
+ )
+
+ body: List[str] = []
+ for service in services:
+ body.extend(self.generate_dart_grpc_client(service))
+ body.append("")
+ body.extend(self.generate_dart_grpc_service_base(service))
+ body.append("")
+
+ lines: List[str] = []
+ lines.append(self.get_license_header("//"))
+ lines.append("")
+ lines.append(
+ "// ignore_for_file: camel_case_types, constant_identifier_names, "
+ "non_constant_identifier_names"
+ )
+ lines.append("")
+ lines.append("import 'dart:async';")
+ lines.append("import 'dart:typed_data';")
+ lines.append("")
+ lines.append("import 'package:grpc/grpc.dart';")
+ lines.append("")
+ lines.append(f"import '{models_stem}.dart' as
{self._grpc_model_alias};")
+ for path, alias in sorted(self._grpc_payload_imports.values()):
+ lines.append(f"import '{path}' as {alias};")
+ lines.append("")
+ lines.append(
+ "// grpc-dart Service self-registers via $methods; "
+ "no separate registration helper needed."
+ )
+ lines.append("")
+ fory = f"{self._grpc_model_alias}.{self.module_type_name()}.getFory()"
+ lines.append("List<int> _serialize<T>(T value) =>")
+ lines.append(f" {fory}.serialize(value, trackRef: true);")
+ lines.append("")
+ lines.append("T _deserialize<T>(List<int> bytes) {")
+ lines.append(
+ " final u8 = bytes is Uint8List ? bytes :
Uint8List.fromList(bytes);"
+ )
+ lines.append(f" return {fory}.deserialize<T>(u8);")
+ lines.append("}")
+ lines.append("")
+ lines.extend(body)
+
+ return GeneratedFile(path=grpc_path, content="\n".join(lines))
+
+ def _dart_grpc_type_ref(self, named_type) -> str:
+ """Return the Dart reference for an RPC request/response type.
+
+ Resolves through the Dart generator's type machinery so nested types
+ use their flattened symbol (`Envelope.Request` -> `Envelope_Request`)
+ and imported types use an alias-qualified reference plus an emitted
+ import, instead of the raw IDL name.
+ """
+ type_def = self.resolve_type(named_type.name)
+ if type_def is None:
+ return f"{self._grpc_model_alias}.{named_type.name}"
+ if self.is_imported_type(type_def):
+ alias = self._dart_grpc_import_alias(type_def)
+ return f"{alias}.{self.local_name(type_def)}"
+ return f"{self._grpc_model_alias}.{self.local_name(type_def)}"
+
+ def _dart_grpc_import_alias(self, type_def) -> str:
+ file = type_def.location.file
+ existing = self._grpc_payload_imports.get(file)
+ if existing is not None:
+ return existing[1]
+ schema = self._load_schema(file)
+ if schema is None:
+ return self.ref_name(type_def)
+ candidate = self.safe_identifier(
+ schema.package.replace(".", "_") if schema.package else
Path(file).stem
+ )
+ alias = self._dart_grpc_unique_import_alias(candidate)
+ self._grpc_payload_imports[file] =
(self._relative_import_path(schema), alias)
+ return alias
+
+ def _dart_grpc_unique_import_alias(self, candidate: str) -> str:
+ alias = candidate
+ index = 2
+ while alias in self._grpc_used_import_aliases:
+ alias = f"{candidate}_{index}"
+ index += 1
+ self._grpc_used_import_aliases.add(alias)
+ return alias
+
+ def _dart_grpc_reserved_import_aliases(self, services: List[Service]) ->
Set[str]:
+ aliases = set(self._DART_GRPC_SUPPORT_NAMES)
+ aliases.update({self._grpc_model_alias, "_serialize", "_deserialize"})
+ for service in services:
+ aliases.add(f"{service.name}Client")
+ aliases.add(f"{service.name}ServiceBase")
+ for method in service.methods:
+ method_name = self.dart_grpc_method_name(method)
+ aliases.add(method_name)
+ aliases.add(f"{method_name}_Pre")
+ return aliases
+
+ def generate_dart_grpc_client(self, service: Service) -> List[str]:
+ lines: List[str] = []
+ lines.append(f"class {service.name}Client extends Client {{")
+ for method in service.methods:
+ method_const = f"_${self.dart_grpc_method_name(method)}"
+ req_t = self._dart_grpc_type_ref(method.request_type)
+ res_t = self._dart_grpc_type_ref(method.response_type)
+ full_path = self.get_grpc_method_path(service, method)
+ lines.append(f" static final {method_const} =")
+ lines.append(f" ClientMethod<{req_t}, {res_t}>(")
+ lines.append(f" '{full_path}',")
+ lines.append(" _serialize,")
+ lines.append(" _deserialize,")
+ lines.append(" );")
+ lines.append("")
+ lines.append(
+ f" {service.name}Client(super.channel, "
+ "{super.options, super.interceptors});"
+ )
+ for method in service.methods:
+ lines.append("")
+ lines.extend(self._dart_grpc_client_method(method))
+ lines.append("}")
+ return lines
+
+ def _dart_grpc_client_method(self, method: RpcMethod) -> List[str]:
+ streaming_request, streaming_response =
self._dart_grpc_call_kind(method)
+ method_const = f"_${self.dart_grpc_method_name(method)}"
+ req_t = self._dart_grpc_type_ref(method.request_type)
+ res_t = self._dart_grpc_type_ref(method.response_type)
+ method_name = self.dart_grpc_method_name(method)
+
+ return_type = (
+ f"ResponseStream<{res_t}>"
+ if streaming_response
+ else (f"ResponseFuture<{res_t}>")
+ )
+ request_param = (
+ f"Stream<{req_t}> request" if streaming_request else (f"{req_t}
request")
+ )
+
+ if not streaming_request and not streaming_response:
+ call_fn = "$createUnaryCall"
+ request_arg = "request"
+ single = ""
+ else:
+ call_fn = "$createStreamingCall"
+ request_arg = "request" if streaming_request else
"Stream.value(request)"
+ # client-stream returns a single response; ResponseStream.single
+ # adapts the streaming call to ResponseFuture<R>.
+ single = "" if streaming_response else ".single"
+
+ lines: List[str] = []
+ lines.append(f" {return_type} {method_name}(")
+ lines.append(f" {request_param}, {{")
+ lines.append(" CallOptions? options,")
+ lines.append(" }) {")
+ lines.extend(
+ self._dart_grpc_call_body(call_fn, method_const, request_arg,
single)
+ )
+ lines.append(" }")
+ return lines
+
+ def _dart_grpc_call_body(
+ self, call_fn: str, method_const: str, request_arg: str, single: str
+ ) -> List[str]:
+ """Emit `return <call>;`, wrapping when dart format would.
+
+ dart format keeps the call on one line when it fits in 80 columns and
+ otherwise wraps each argument onto its own line with a trailing comma.
+ Matching that here keeps the emitted file format-clean.
+ """
+ single_line = (
+ f" return {call_fn}({method_const}, {request_arg}, "
+ f"options: options){single};"
+ )
+ if len(single_line) <= 80:
+ return [single_line]
+ return [
+ f" return {call_fn}(",
+ f" {method_const},",
+ f" {request_arg},",
+ " options: options,",
+ f" ){single};",
+ ]
+
+ def _dart_grpc_call_kind(self, method: RpcMethod):
+ """Return (streaming_request, streaming_response) for an RPC method."""
+ return bool(method.client_streaming), bool(method.server_streaming)
+
+ def generate_dart_grpc_service_base(self, service: Service) -> List[str]:
+ lines: List[str] = []
+ lines.append(f"abstract class {service.name}ServiceBase extends
Service {{")
+ lines.append(" @override")
+ lines.append(f" String get $name =>
'{self.get_grpc_service_name(service)}';")
+ lines.append("")
+ lines.append(f" {service.name}ServiceBase() {{")
+ for method in service.methods:
+ streaming_request, streaming_response =
self._dart_grpc_call_kind(method)
+ req_t = self._dart_grpc_type_ref(method.request_type)
+ res_t = self._dart_grpc_type_ref(method.response_type)
+ method_name = self.dart_grpc_method_name(method)
+ lines.append(" $addMethod(")
+ lines.append(f" ServiceMethod<{req_t}, {res_t}>(")
+ lines.append(f" '{method.name}',")
+ lines.append(f" {method_name}_Pre,")
+ lines.append(f" {str(streaming_request).lower()},")
+ lines.append(f" {str(streaming_response).lower()},")
+ lines.append(f" (List<int> value) =>
_deserialize<{req_t}>(value),")
+ lines.append(f" ({res_t} value) => _serialize(value),")
+ lines.append(" ),")
+ lines.append(" );")
+ lines.append(" }")
+ lines.append("")
+ for idx, method in enumerate(service.methods):
+ lines.extend(self._dart_grpc_service_method(method))
+ if idx != len(service.methods) - 1:
+ lines.append("")
+ lines.append("}")
+ return lines
+
+ def _dart_grpc_service_method(self, method: RpcMethod) -> List[str]:
+ streaming_request, streaming_response =
self._dart_grpc_call_kind(method)
+ req_t = self._dart_grpc_type_ref(method.request_type)
+ res_t = self._dart_grpc_type_ref(method.response_type)
+ method_name = self.dart_grpc_method_name(method)
+
+ # grpc-dart hands the handler a Future<Q> for a single request and a
+ # Stream<Q> for a client-streaming request, and consumes the handler's
+ # return value directly as the response (a Future<R> for a single
+ # response, a Stream<R> for a streaming response). The _Pre shim adapts
+ # that handler signature to the user-overridable method:
+ # - single request -> await $request before delegating
+ # - stream request -> pass $request straight through
+ # - stream response -> the shim is async* and yield*s the delegate
+ user_return = f"Stream<{res_t}>" if streaming_response else
f"Future<{res_t}>"
+ user_param = f"Stream<{req_t}>" if streaming_request else req_t
+ shim_param = f"Stream<{req_t}>" if streaming_request else
f"Future<{req_t}>"
+ request_arg = "$request" if streaming_request else "await $request"
+
+ lines: List[str] = []
+ lines.append(
+ " // protoc_plugin parity: _Pre shim adapts the grpc-dart handler"
+ )
+ lines.append(" // signature to the user-overridable method.")
+ lines.append(f" {user_return} {method_name}_Pre(")
+ lines.append(" ServiceCall $call,")
+ lines.append(f" {shim_param} $request,")
+ if streaming_response and not streaming_request:
+ # server-stream: must await the single request, then stream
results.
+ lines.append(" ) async* {")
+ lines.append(f" yield* {method_name}($call, {request_arg});")
+ elif not streaming_request:
+ # unary: await the single request, return the single response.
+ lines.append(" ) async {")
+ lines.append(f" return {method_name}($call, {request_arg});")
+ else:
+ # client-stream / bidi: pass the request stream straight through.
+ lines.append(" ) {")
+ lines.append(f" return {method_name}($call, {request_arg});")
+ lines.append(" }")
+ lines.append("")
+ lines.append(f" {user_return} {method_name}(")
+ lines.append(" ServiceCall call,")
+ lines.append(f" {user_param} request,")
+ lines.append(" );")
+ return lines
+
+ def dart_grpc_method_name(self, method: RpcMethod) -> str:
+ return self.safe_identifier(self.to_camel_case(method.name))
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py
b/compiler/fory_compiler/tests/test_service_codegen.py
index b7d2d2a97..2d63b2ece 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -44,6 +44,7 @@ from fory_compiler.frontend.proto.translator import
ProtoTranslator
from fory_compiler.generators.base import BaseGenerator, GeneratorOptions
from fory_compiler.generators.cpp import CppGenerator
from fory_compiler.generators.csharp import CSharpGenerator
+from fory_compiler.generators.dart import DartGenerator
from fory_compiler.generators.go import GoGenerator
from fory_compiler.generators.java import JavaGenerator
from fory_compiler.generators.javascript import JavaScriptGenerator
@@ -67,6 +68,7 @@ GENERATOR_CLASSES: Tuple[Type[BaseGenerator], ...] = (
SwiftGenerator,
ScalaGenerator,
KotlinGenerator,
+ DartGenerator,
)
_GREETER_WITH_SERVICE = dedent(
@@ -155,6 +157,7 @@ def test_unsupported_generators_no_services():
ScalaGenerator,
KotlinGenerator,
JavaScriptGenerator,
+ DartGenerator,
):
continue
options = GeneratorOptions(output_dir=Path("/tmp"))
@@ -2968,3 +2971,634 @@ def test_rust_grpc_rejects_unsafe_refs():
)
with pytest.raises(ValueError, match=message):
generator.generate_services()
+
+
+def test_dart_grpc_streaming_shapes():
+ from fory_compiler.generators.dart import DartGenerator
+
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.streams;
+
+ message Req {}
+ message Res {}
+
+ service Streamer {
+ rpc UnaryMessage (Req) returns (Res);
+ rpc ServerStreamMessage (Req) returns (stream Res);
+ rpc ClientStreamMessage (stream Req) returns (Res);
+ rpc BidiStreamMessage (stream Req) returns (stream Res);
+ }
+ """
+ )
+ )
+
+ content = generate_service_files(schema, DartGenerator)[
+ "demo/streams/demo_streams_grpc.dart"
+ ]
+
+ assert "ResponseFuture<_models.Res> unaryMessage(" in content
+ assert "$createUnaryCall(_$unaryMessage, request, options: options);" in
content
+
+ assert "ResponseStream<_models.Res> serverStreamMessage(" in content
+ assert (
+ " return $createStreamingCall(\n"
+ " _$serverStreamMessage,\n"
+ " Stream.value(request),\n"
+ " options: options,\n"
+ " );"
+ ) in content
+
+ assert "ResponseFuture<_models.Res> clientStreamMessage(" in content
+ assert "Stream<_models.Req> request, {" in content
+ assert (
+ " return $createStreamingCall(\n"
+ " _$clientStreamMessage,\n"
+ " request,\n"
+ " options: options,\n"
+ " ).single;"
+ ) in content
+
+ assert "ResponseStream<_models.Res> bidiStreamMessage(" in content
+ assert (
+ "$createStreamingCall(_$bidiStreamMessage, request, options: options);"
+ ) in content
+
+ assert (
+ "'UnaryMessage',\n unaryMessage_Pre,\n false,\n
false,"
+ ) in content
+ assert (
+ "'ServerStreamMessage',\n serverStreamMessage_Pre,\n"
+ " false,\n true,"
+ ) in content
+ assert (
+ "'ClientStreamMessage',\n clientStreamMessage_Pre,\n"
+ " true,\n false,"
+ ) in content
+ assert (
+ "'BidiStreamMessage',\n bidiStreamMessage_Pre,\n"
+ " true,\n true,"
+ ) in content
+
+ assert "Future<_models.Res> unaryMessage_Pre(" in content
+ assert "Stream<_models.Res> serverStreamMessage_Pre(" in content
+ assert (
+ " ) async* {\n yield* serverStreamMessage($call, await $request);"
+ ) in content
+ assert "Future<_models.Res> clientStreamMessage_Pre(" in content
+ assert " ) {\n return clientStreamMessage($call, $request);" in content
+ assert "Stream<_models.Res> bidiStreamMessage_Pre(" in content
+
+ assert (
+ "Future<_models.Res> unaryMessage(\n ServiceCall call,\n"
+ " _models.Req request,\n );"
+ ) in content
+ assert (
+ "Stream<_models.Res> serverStreamMessage(\n ServiceCall call,\n"
+ " _models.Req request,\n );"
+ ) in content
+ assert (
+ "Future<_models.Res> clientStreamMessage(\n ServiceCall call,\n"
+ " Stream<_models.Req> request,\n );"
+ ) in content
+ assert (
+ "Stream<_models.Res> bidiStreamMessage(\n ServiceCall call,\n"
+ " Stream<_models.Req> request,\n );"
+ ) in content
+
+
+def test_dart_grpc_fory_codec():
+ from fory_compiler.generators.dart import DartGenerator
+
+ schema = parse_fdl(_GREETER_WITH_SERVICE)
+ files = generate_service_files(schema, DartGenerator)
+ assert set(files) == {"demo/greeter/demo_greeter_grpc.dart"}
+ content = files["demo/greeter/demo_greeter_grpc.dart"]
+
+ assert "This file is generated by Apache Fory compiler." in content
+ assert "library;" not in content
+
+ assert "import 'dart:typed_data';" in content
+ assert "import 'package:grpc/grpc.dart';" in content
+ assert "import 'demo_greeter.dart' as _models;" in content
+
+ assert (
+ "_models.DemoGreeterForyModule.getFory().serialize(value, trackRef:
true)"
+ in content
+ )
+ assert "_models.DemoGreeterForyModule.getFory().deserialize<T>" in content
+ assert "is Uint8List ? bytes : Uint8List.fromList(bytes)" in content
+
+ assert "class GreeterClient extends Client {" in content
+ assert "static final _$sayHello =" in content
+ assert "ClientMethod<_models.HelloRequest, _models.HelloReply>(" in content
+ assert "'/demo.greeter.Greeter/SayHello'," in content
+ assert (
+ "GreeterClient(super.channel, {super.options, super.interceptors});"
in content
+ )
+ assert "ResponseFuture<_models.HelloReply> sayHello(" in content
+ assert "_models.HelloRequest request, {" in content
+ assert "$createUnaryCall(_$sayHello, request, options: options);" in
content
+
+ assert "abstract class GreeterServiceBase extends Service {" in content
+ assert "String get $name => 'demo.greeter.Greeter';" in content
+ assert "GreeterServiceBase() {" in content
+ assert "$addMethod(" in content
+ assert ("ServiceMethod<_models.HelloRequest, _models.HelloReply>(") in
content
+ assert "'SayHello'," in content
+ assert "sayHello_Pre," in content
+ assert "Future<_models.HelloReply> sayHello_Pre(" in content
+ assert "Future<_models.HelloReply> sayHello(" in content
+ assert "ServiceCall call," in content
+
+
+def test_dart_grpc_service_class_collision():
+ from fory_compiler.generators.dart import DartGenerator
+
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.collide;
+
+ message GreeterClient {
+ string name = 1;
+ }
+
+ message HelloRequest {}
+ message HelloReply {}
+
+ service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ }
+ """
+ )
+ )
+
+ import pytest
+
+ with pytest.raises(ValueError) as excinfo:
+ generate_service_files(schema, DartGenerator)
+ msg = str(excinfo.value)
+ assert "GreeterClient" in msg
+ assert "conflicts" in msg
+
+
+def test_dart_grpc_method_collision():
+ from fory_compiler.generators.dart import DartGenerator
+
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.dupes;
+
+ message Req {}
+ message Res {}
+
+ service Greeter {
+ rpc SayHello (Req) returns (Res);
+ rpc say_hello (Req) returns (Res);
+ }
+ """
+ )
+ )
+
+ import pytest
+
+ with pytest.raises(ValueError) as excinfo:
+ generate_service_files(schema, DartGenerator)
+ msg = str(excinfo.value)
+ assert "Greeter" in msg
+ assert "SayHello" in msg
+ assert "say_hello" in msg
+ assert "sayHello" in msg
+
+
+def test_dart_grpc_proto_fbs():
+ from fory_compiler.generators.dart import DartGenerator
+
+ proto_schema = parse_proto(
+ dedent(
+ """
+ syntax = "proto3";
+ package demo.proto;
+
+ message Req {}
+ message Res {}
+
+ service ProtoSvc {
+ rpc Call (Req) returns (Res);
+ }
+ """
+ )
+ )
+ proto_dart = generate_service_files(proto_schema, DartGenerator)
+ assert "demo/proto/demo_proto_grpc.dart" in proto_dart
+ assert (
+ "'/demo.proto.ProtoSvc/Call'," in
proto_dart["demo/proto/demo_proto_grpc.dart"]
+ )
+ assert (
+ "class ProtoSvcClient extends Client {"
+ in proto_dart["demo/proto/demo_proto_grpc.dart"]
+ )
+
+ fbs_schema = parse_fbs(
+ dedent(
+ """
+ namespace demo.fbs;
+
+ table Req {}
+ table Res {}
+
+ rpc_service FbsSvc {
+ Call(Req):Res;
+ }
+ """
+ )
+ )
+ fbs_dart = generate_service_files(fbs_schema, DartGenerator)
+ assert "demo/fbs/demo_fbs_grpc.dart" in fbs_dart
+ assert "'/demo.fbs.FbsSvc/Call'," in
fbs_dart["demo/fbs/demo_fbs_grpc.dart"]
+ assert (
+ "class FbsSvcClient extends Client {" in
fbs_dart["demo/fbs/demo_fbs_grpc.dart"]
+ )
+
+
+def test_dart_grpc_nested_rpc_payloads():
+ from fory_compiler.generators.dart import DartGenerator
+
+ schema = parse_fdl(
+ dedent(
+ """
+ package demo.nested;
+
+ message Envelope {
+ message Request {
+ string name = 1;
+ }
+ message Reply {
+ string name = 1;
+ }
+ }
+
+ service Nested {
+ rpc Call (Envelope.Request) returns (Envelope.Reply);
+ }
+ """
+ )
+ )
+ content = generate_service_files(schema, DartGenerator)[
+ "demo/nested/demo_nested_grpc.dart"
+ ]
+ assert "ClientMethod<_models.Envelope_Request, _models.Envelope_Reply>("
in content
+ assert "Future<_models.Envelope_Reply> call(" in content
+ assert "_models.Envelope.Request" not in content
+
+
+def test_dart_grpc_imported_rpc_payloads(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package demo.common;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ service Api {
+ rpc Call (demo.common.Shared) returns (demo.common.Shared);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../common/common.dart' as demo_common;" in content
+ assert "ClientMethod<demo_common.Shared, demo_common.Shared>(" in content
+ assert "_models.demo.common.Shared" not in content
+
+
+def test_dart_imported_model_collision(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package demo.common;
+
+ message GreeterClient {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ message Req {}
+ message Res {}
+
+ service Greeter {
+ rpc Call (Req) returns (Res);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "class GreeterClient extends Client {" in content
+ assert "ClientMethod<_models.Req, _models.Res>(" in content
+
+
+def test_dart_grpc_import_alias_collision(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package _models;
+
+ message Req {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ message Req {}
+ message Res {}
+
+ service Api {
+ rpc Call (_models.Req) returns (Res);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import 'api.dart' as _models;" in content
+ assert "import '../../_models/_models.dart' as _models_2;" in content
+ assert "ClientMethod<_models_2.Req, _models.Res>(" in content
+ assert "ServiceMethod<_models_2.Req, _models.Res>(" in content
+
+
+def test_dart_grpc_helper_alias(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package _serialize;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ message Res {}
+
+ service Api {
+ rpc Call (_serialize.Shared) returns (Res);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../../_serialize/_serialize.dart' as _serialize_2;" in
content
+ assert "ClientMethod<_serialize_2.Shared, _models.Res>(" in content
+ assert "ServiceMethod<_serialize_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_service_alias(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package ApiClient;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ message Res {}
+
+ service Api {
+ rpc Call (ApiClient.Shared) returns (Res);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../../ApiClient/ApiClient.dart' as ApiClient_2;" in content
+ assert "class ApiClient extends Client {" in content
+ assert "ClientMethod<ApiClient_2.Shared, _models.Res>(" in content
+ assert "ServiceMethod<ApiClient_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_support_aliases(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ common = tmp_path / "common.fdl"
+ common.write_text(
+ dedent(
+ """
+ package Client;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "common.fdl";
+
+ message Res {}
+
+ service Api {
+ rpc Call (Client.Shared) returns (Res);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../../Client/Client.dart' as Client_2;" in content
+ assert "class ApiClient extends Client {" in content
+ assert "ClientMethod<Client_2.Shared, _models.Res>(" in content
+ assert "ServiceMethod<Client_2.Shared, _models.Res>(" in content
+
+
+def test_dart_grpc_local_aliases(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ value_schema = tmp_path / "value.fdl"
+ value_schema.write_text(
+ dedent(
+ """
+ package value;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "value.fdl";
+
+ service Api {
+ rpc Echo (Shared) returns (Shared);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../../value/value.dart' as value_2;" in content
+ assert "ClientMethod<value_2.Shared, value_2.Shared>(" in content
+ assert "ServiceMethod<value_2.Shared, value_2.Shared>(" in content
+ assert "_deserialize<value_2.Shared>(value)" in content
+ assert "(value_2.Shared value) => _serialize(value)" in content
+ assert "_models.value.Shared" not in content
+
+
+def test_dart_grpc_method_aliases(tmp_path: Path):
+ from fory_compiler.generators.dart import DartGenerator
+
+ foo_schema = tmp_path / "foo.fdl"
+ foo_schema.write_text(
+ dedent(
+ """
+ package foo;
+
+ message Shared {
+ string id = 1;
+ }
+ """
+ )
+ )
+ service = tmp_path / "service.fdl"
+ service.write_text(
+ dedent(
+ """
+ package demo.api;
+
+ import "foo.fdl";
+
+ service Api {
+ rpc Foo (Shared) returns (Shared);
+ }
+ """
+ )
+ )
+ schema = resolve_imports(service)
+ generator = DartGenerator(schema, GeneratorOptions(output_dir=tmp_path,
grpc=True))
+ content = generator.generate_services()[0].content
+
+ assert "import '../../foo/foo.dart' as foo_2;" in content
+ assert "ResponseFuture<foo_2.Shared> foo(" in content
+ assert "Future<foo_2.Shared> foo_Pre(" in content
+ assert "foo.Shared" not in content
+
+
+def test_dart_grpc_reserved_methods():
+ from fory_compiler.generators.dart import DartGenerator
+
+ import pytest
+
+ for rpc_name, emitted in [("ToString", "toString"), ("HashCode",
"hashCode")]:
+ schema = parse_fdl(
+ dedent(
+ f"""
+ package demo.names;
+
+ message Req {{}}
+ message Res {{}}
+
+ service Svc {{
+ rpc {rpc_name} (Req) returns (Res);
+ }}
+ """
+ )
+ )
+ with pytest.raises(ValueError) as excinfo:
+ generate_service_files(schema, DartGenerator)
+ msg = str(excinfo.value)
+ assert "inherited Dart member" in msg
+ assert f"Svc.{rpc_name} -> {emitted}" in msg
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 0c5ebd491..89a2427b6 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -143,16 +143,16 @@ foryc schema.fdl --output ./src/generated
foryc user.fdl order.fdl product.fdl --output ./generated
```
-**Compile a simple schema containing service definitions (Java + Python + Go +
Rust + C# + Scala + Kotlin + JavaScript models):**
+**Compile a simple schema containing service definitions (Java + Python + Go +
Rust + C# + Dart + Scala + Kotlin + JavaScript models):**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --go_out=./generated/go
--rust_out=./generated/rust --csharp_out=./generated/csharp
--scala_out=./generated/scala --kotlin_out=./generated/kotlin
--javascript_out=./generated/javascript
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --go_out=./generated/go
--rust_out=./generated/rust --csharp_out=./generated/csharp
--dart_out=./generated/dart --scala_out=./generated/scala
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript
```
-**Generate Java, Python, Go, Rust, C#, Scala, Kotlin, and Node.js JavaScript
gRPC service companions:**
+**Generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and Node.js
JavaScript gRPC service companions:**
```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --go_out=./generated/go
--rust_out=./generated/rust --csharp_out=./generated/csharp
--scala_out=./generated/scala --kotlin_out=./generated/kotlin
--javascript_out=./generated/javascript --grpc
+foryc compiler/examples/service.fdl --java_out=./generated/java
--python_out=./generated/python --go_out=./generated/go
--rust_out=./generated/rust --csharp_out=./generated/csharp
--dart_out=./generated/dart --scala_out=./generated/scala
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
```
The generated gRPC service code uses Fory to serialize request and response
@@ -161,7 +161,8 @@ payloads. Java output imports grpc-java APIs, Python output
defaults to
Scala output imports grpc-java APIs, and Kotlin output imports grpc-java and
grpc-kotlin APIs and uses coroutine stubs. C# output imports `Grpc.Core.Api`
types and can be hosted with normal .NET gRPC packages such as
`Grpc.AspNetCore`
-or called through `Grpc.Net.Client`. JavaScript output imports `@grpc/grpc-js`.
+or called through `Grpc.Net.Client`. Dart output imports `package:grpc`.
+JavaScript output imports `@grpc/grpc-js`.
Applications that compile or run those generated service files must provide
their own gRPC dependencies. Fory packages do not add a hard gRPC dependency
for
this feature.
@@ -440,6 +441,9 @@ generated/
- IDL module class included in the main file; generated serializer metadata is
included in the part file
- Typed arrays used for non-optional, non-ref primitive lists (e.g.,
`Int32List`)
+- With `--grpc`, one `<stem>_grpc.dart` companion per schema is generated next
to
+ the model file, containing each service's `Client` and `ServiceBase`; it
+ imports `package:grpc`
### Scala
diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md
index 89b375153..0ad11a648 100644
--- a/docs/compiler/flatbuffers-idl.md
+++ b/docs/compiler/flatbuffers-idl.md
@@ -126,8 +126,8 @@ message Container {
FlatBuffers `rpc_service` definitions are translated to Fory services. With
`--grpc`, the compiler emits gRPC service companions for supported outputs such
-as Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript. JavaScript
browser
-clients are generated with `--grpc-web`. These companions use Fory
+as Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and JavaScript. JavaScript
+browser clients are generated with `--grpc-web`. These companions use Fory
serialization for request and response payloads.
```fbs
@@ -138,16 +138,17 @@ rpc_service SearchService {
```
```bash
-foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --scala_out=./generated/scala
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc api.fbs --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --dart_out=./generated/dart
--scala_out=./generated/scala --kotlin_out=./generated/kotlin
--javascript_out=./generated/javascript --grpc
```
Generated service code imports grpc APIs, so applications must provide
grpc-java,
grpc-kotlin, Scala grpc-java APIs, `grpcio`, grpc-go, Rust `tonic` and `bytes`,
-`@grpc/grpc-js`, or C# `Grpc.Core.Api` plus server/client dependencies when
they
-compile or run those files. Python companions use `grpc.aio` by default and can
-be generated in sync mode with `--grpc-python-mode=sync`. Fory packages do not
-add gRPC as a hard dependency. Use `--grpc-web` with JavaScript output to
-generate browser clients that import `grpc-web`.
+`@grpc/grpc-js`, C# `Grpc.Core.Api` plus server/client dependencies, or Dart
+`package:grpc` when they compile or run those files. Python companions use
+`grpc.aio` by default and can be generated in sync mode with
+`--grpc-python-mode=sync`. Fory packages do not add gRPC as a hard dependency.
+Use `--grpc-web` with JavaScript output to generate browser clients that import
+`grpc-web`.
### Defaults and Metadata
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index 1f52db67e..b3e5ebc91 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1380,6 +1380,47 @@ void main() {
}
```
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, Dart
+generation emits one `<module>_grpc.dart` file per schema next to the model
+types. It targets `package:grpc`. Request and response serialization uses a
Fory
+runtime the companion obtains automatically and that registers the schema's
+types on first use, so no manual registration is required; an application may
+optionally inject a custom `Fory` via the schema module's `install(...)` before
+the first call.
+
+All four RPC modes are generated: unary, server-streaming, client-streaming,
and
+bidirectional. The client class extends `Client`; the service base class
extends
+`Service` and self-registers each method with `$addMethod`.
+
+```dart
+class GreeterClient extends Client {
+ // Single response: ResponseFuture. Streaming response: ResponseStream.
+ ResponseFuture<HelloReply> sayHello(HelloRequest request, {CallOptions?
options}) { ... }
+ ResponseStream<HelloReply> sayHellos(HelloRequest request, {CallOptions?
options}) { ... }
+ ResponseFuture<HelloReply> collectHellos(Stream<HelloRequest> request,
{CallOptions? options}) { ... }
+ ResponseStream<HelloReply> chatHellos(Stream<HelloRequest> request,
{CallOptions? options}) { ... }
+}
+
+abstract class GreeterServiceBase extends Service {
+ Future<HelloReply> sayHello(ServiceCall call, HelloRequest request);
+ Stream<HelloReply> sayHellos(ServiceCall call, HelloRequest request);
+ Future<HelloReply> collectHellos(ServiceCall call, Stream<HelloRequest>
request);
+ Stream<HelloReply> chatHellos(ServiceCall call, Stream<HelloRequest>
request);
+}
+```
+
+A single-response client method returns `ResponseFuture<R>` (client-streaming
+adapts the streaming call with `.single`); a streaming-response method returns
+`ResponseStream<R>`. On the server, implementations override the abstract
methods,
+which receive a single request as `Q` and a client-streaming request as
+`Stream<Q>`, and return a `Future` for single responses or a `Stream` for
+streaming responses.
+Applications compiling these files must provide a `grpc` dependency; the Fory
Dart
+runtime does not add one. The original IDL method names are used in the gRPC
wire
+paths.
+
## Kotlin
The Kotlin target emits Kotlin source only. The compiler does not generate Java
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index da10b2cd1..caa69a6fc 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -23,7 +23,7 @@ Fory IDL is a schema definition language for Apache Fory that
enables type-safe
cross-language serialization. Define your data structures once and generate
native data structure code for Java, Python, C++, Go, Rust,
JavaScript/TypeScript, C#, Swift, Dart, Scala, and Kotlin. Fory IDL can also
-describe RPC services; for Java, Python, Go, Rust, C#, Scala, Kotlin, and
+describe RPC services; for Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and
JavaScript, the compiler can generate gRPC service companions that use Fory
serialization for request and response payloads.
@@ -88,21 +88,21 @@ service AnimalService {
}
```
-Generate Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript models plus
-gRPC service companions with:
+Generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and JavaScript models
+plus gRPC service companions with:
```bash
-foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --scala_out=./generated/scala
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc animals.fdl --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --dart_out=./generated/dart
--scala_out=./generated/scala --kotlin_out=./generated/kotlin
--javascript_out=./generated/javascript --grpc
```
The generated service code uses normal gRPC APIs, but request and response
objects are serialized with Fory. Applications provide their own grpc-java,
grpc-kotlin, Scala grpc-java APIs, `grpcio`, grpc-go, Rust `tonic` and `bytes`,
-or C# `Grpc.Core.Api` and hosting/client dependencies; Fory packages do not add
-gRPC as a hard dependency. Python companions use `grpc.aio` by default and can
-be generated in sync mode with `--grpc-python-mode=sync`. JavaScript Node.js
-companions use `@grpc/grpc-js`; browser clients are generated separately with
-`--grpc-web` and use `grpc-web`.
+C# `Grpc.Core.Api` and hosting/client dependencies, or Dart `package:grpc`;
Fory
+packages do not add gRPC as a hard dependency. Python companions use `grpc.aio`
+by default and can be generated in sync mode with `--grpc-python-mode=sync`.
+JavaScript Node.js companions use `@grpc/grpc-js`; browser clients are
generated
+separately with `--grpc-web` and use `grpc-web`.
## Why Fory IDL?
diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md
index da5deba31..d9355deec 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -41,19 +41,19 @@ how protobuf concepts map to Fory, and how to use
protobuf-only Fory extension o
## Protobuf vs Fory at a Glance
-| Aspect | Protocol Buffers | Fory
|
-| ------------------ | ----------------------------- |
-------------------------------------------------------------- |
-| Primary purpose | RPC/message contracts | High-performance object
serialization |
-| Encoding model | Tag-length-value | Fory binary protocol
|
-| Reference tracking | Not built-in | First-class (`ref`)
|
-| Circular refs | Not supported | Supported
|
-| Unknown fields | Preserved | Not preserved
|
-| Generated types | Protobuf-specific model types | Native language
constructs |
-| gRPC ecosystem | Native |
Java/Python/Go/Rust/C#/Scala/Kotlin/JavaScript service codegen |
-
-Fory can generate Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript
gRPC
-service companions with `--grpc`. JavaScript browser clients are generated with
-`--grpc-web`. Those services use normal gRPC transports but serialize request
+| Aspect | Protocol Buffers | Fory
|
+| ------------------ | ----------------------------- |
------------------------------------------------------------------- |
+| Primary purpose | RPC/message contracts | High-performance object
serialization |
+| Encoding model | Tag-length-value | Fory binary protocol
|
+| Reference tracking | Not built-in | First-class (`ref`)
|
+| Circular refs | Not supported | Supported
|
+| Unknown fields | Preserved | Not preserved
|
+| Generated types | Protobuf-specific model types | Native language
constructs |
+| gRPC ecosystem | Native |
Java/Python/Go/Rust/C#/Dart/Scala/Kotlin/JavaScript service codegen |
+
+Fory can generate Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and
+JavaScript gRPC service companions with `--grpc`. JavaScript browser clients
are
+generated with `--grpc-web`. Those services use normal gRPC transports but
serialize request
and response payloads with Fory rather than protobuf. For broad gRPC ecosystem
tooling, schema reflection, and protobuf-native interceptors, protobuf remains
the mature/default choice.
@@ -315,19 +315,20 @@ languages.
For supported service outputs, add `--grpc` to emit gRPC companion code:
```bash
-foryc api.proto --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --scala_out=./generated/scala
--kotlin_out=./generated/kotlin --javascript_out=./generated/javascript --grpc
+foryc api.proto --java_out=./generated/java --python_out=./generated/python
--go_out=./generated/go --rust_out=./generated/rust
--csharp_out=./generated/csharp --dart_out=./generated/dart
--scala_out=./generated/scala --kotlin_out=./generated/kotlin
--javascript_out=./generated/javascript --grpc
```
Generated Java service files compile against grpc-java, generated Python
service
modules use `grpc.aio` by default, generated Rust service files import `tonic`
and `bytes`, generated Go service files import grpc-go, generated JavaScript
Node.js service files import `@grpc/grpc-js`, generated C# service files import
-`Grpc.Core.Api` types, generated Scala service files compile against grpc-java,
-and generated Kotlin service files compile against grpc-java and grpc-kotlin.
-Add those dependencies in your application build; Fory packages do not add gRPC
-as a hard dependency. Use `--grpc-python-mode=sync` for sync Python `grpcio`
-companions. Use `--grpc-web` with JavaScript output to generate browser clients
-that import `grpc-web`.
+`Grpc.Core.Api` types, generated Dart service files import `package:grpc`,
+generated Scala service files compile against grpc-java, and generated Kotlin
+service files compile against grpc-java and grpc-kotlin. Add those dependencies
+in your application build; Fory packages do not add gRPC as a hard dependency.
+Use `--grpc-python-mode=sync` for sync Python `grpcio` companions. Use
+`--grpc-web` with JavaScript output to generate browser clients that import
+`grpc-web`.
Protobuf `oneof` fields are translated to Fory union fields inside request and
response messages. Direct union RPC request or response types are not part of
normal protobuf RPC syntax.
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index d2e12c989..633b90f1e 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -908,7 +908,7 @@ union_field := ['repeated'] field_type IDENTIFIER '='
INTEGER [field_options] ';
Services define RPC method contracts in Fory IDL. They are optional: schemas
with services still generate the normal data model types, and gRPC service code
is generated only when the compiler is run with `--grpc` for supported language
-outputs such as Java, Python, Go, Rust, C#, Scala, Kotlin, and JavaScript.
+outputs such as Java, Python, Go, Rust, C#, Dart, Scala, Kotlin, and
JavaScript.
JavaScript browser gRPC-Web clients are generated with `--grpc-web`.
```protobuf
@@ -952,9 +952,9 @@ service PetDirectory {
- The generated gRPC companions use Fory serialization for each RPC payload.
Applications that compile or run those companions provide their own gRPC
dependency, such as grpc-java, grpc-kotlin, `grpcio`, grpc-go, Rust `tonic`
- and `bytes`, Scala grpc-java APIs, `@grpc/grpc-js`, `grpc-web`, or C#
- `Grpc.Core.Api` plus a server or client package. Python companions use
- `grpc.aio` by default and can be generated in sync mode with
+ and `bytes`, Scala grpc-java APIs, `@grpc/grpc-js`, `grpc-web`, C#
+ `Grpc.Core.Api` plus a server or client package, or Dart `package:grpc`.
Python
+ companions use `grpc.aio` by default and can be generated in sync mode with
`--grpc-python-mode=sync`.
**Grammar:**
diff --git a/docs/guide/dart/grpc-support.md b/docs/guide/dart/grpc-support.md
new file mode 100644
index 000000000..cda566adc
--- /dev/null
+++ b/docs/guide/dart/grpc-support.md
@@ -0,0 +1,292 @@
+---
+title: gRPC Support
+sidebar_position: 12
+id: grpc_support
+license: |
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+---
+
+Fory can generate Dart gRPC service companions for schemas that define
services.
+The generated code uses normal `package:grpc` clients, service bases, method
+descriptors, call options, deadlines, cancellations, and status codes, while
+request and response objects are serialized with Fory instead of protobuf.
+
+Use this mode when both RPC peers are generated from the same Fory IDL,
protobuf
+IDL, or FlatBuffers IDL and both sides expect Fory-encoded message bodies. Use
+normal protobuf gRPC generation for APIs that must be consumed by generic
+protobuf clients, reflection tools, or components that expect protobuf message
+bytes.
+
+## Add Dependencies
+
+The `fory` package does not add gRPC dependencies. Add `grpc` (and the
+`build_runner` dev dependency that generates the Fory serializer code) in the
+application that compiles or runs generated service companions:
+
+```yaml
+dependencies:
+ fory: ^1.1.0
+ grpc: ^4.0.0
+
+dev_dependencies:
+ build_runner: ^2.4.0
+```
+
+The same dependencies cover both client and server applications.
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string reply = 1;
+}
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate Dart model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --dart_out=./lib/generated --grpc
+```
+
+Then run `build_runner` once to emit the Fory serializer part file for the
+generated models (this step is required before the code can run):
+
+```bash
+dart run build_runner build --delete-conflicting-outputs
+```
+
+For this schema, the Dart generator emits (the model file and module are named
+from the package leaf, `greeter`):
+
+| File | Purpose
|
+| ------------------------------------------- |
---------------------------------------------------- |
+| `demo/greeter/greeter.dart` | Fory model types and the
schema module |
+| `demo/greeter/greeter.fory.dart` | Serializers and registration
(built by build_runner) |
+| `demo/greeter/greeter_grpc.dart` | gRPC client, service base, and
method descriptors |
+| `GreeterForyModule` in `greeter.dart` | Fory registration module for
generated types |
+| `GreeterServiceBase` in `greeter_grpc.dart` | Base class for server
implementations |
+| `GreeterClient` in `greeter_grpc.dart` | Client stub for gRPC calls
|
+
+The generated client and service base obtain a ready `Fory` automatically and
+register the schema's types on first use, so no manual registration step is
+required. To share a custom `Fory` (for example one configured with extra
+modules), call `GreeterForyModule.install(yourFory)` once before the first RPC;
+this is optional.
+
+## Implement a Server
+
+Extend the generated `GreeterServiceBase` and host it with grpc-dart's
`Server`:
+
+```dart
+import 'dart:io';
+
+import 'package:grpc/grpc.dart';
+import 'demo/greeter/greeter.dart';
+import 'demo/greeter/greeter_grpc.dart';
+
+class GreeterService extends GreeterServiceBase {
+ @override
+ Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async {
+ final reply = HelloReply()..reply = 'Hello, ${request.name}';
+ return reply;
+ }
+}
+
+Future<void> main() async {
+ final server = Server.create(services: [GreeterService()]);
+ await server.serve(address: InternetAddress.loopbackIPv4, port: 50051);
+}
+```
+
+## Create a Client
+
+Use the generated client with a `ClientChannel`:
+
+```dart
+import 'package:grpc/grpc.dart';
+import 'demo/greeter/greeter.dart';
+import 'demo/greeter/greeter_grpc.dart';
+
+Future<void> main() async {
+ final channel = ClientChannel(
+ 'localhost',
+ port: 50051,
+ options: const ChannelOptions(
+ credentials: ChannelCredentials.insecure(),
+ ),
+ );
+ final client = GreeterClient(channel);
+
+ final reply = await client.sayHello(HelloRequest()..name = 'Fory');
+ print(reply.reply);
+
+ await channel.shutdown();
+}
+```
+
+## Streaming RPCs
+
+Fory service definitions can use the same gRPC streaming shapes:
+
+```protobuf
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+ rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+ rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+ rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated Dart methods follow grpc-dart conventions. Single responses return a
+`ResponseFuture<R>` (client-streaming adapts the call with `.single`);
streaming
+responses return a `ResponseStream<R>`. On the server, single requests arrive
as
+the message type and streaming requests as a `Stream`; the method returns a
+`Future` for single responses and a `Stream` for streaming responses:
+
+| IDL shape | Client method
| Server method (override) |
+| ----------------------------------------- |
---------------------------------------------------- |
------------------------------------------------------ |
+| `rpc A (Req) returns (Res)` | `ResponseFuture<Res> a(Req
request, {CallOptions?})` | `Future<Res> a(ServiceCall call, Req request)`
|
+| `rpc A (Req) returns (stream Res)` | `ResponseStream<Res> a(Req
request, {CallOptions?})` | `Stream<Res> a(ServiceCall call, Req request)`
|
+| `rpc A (stream Req) returns (Res)` | `ResponseFuture<Res>
a(Stream<Req> request, {...})` | `Future<Res> a(ServiceCall call, Stream<Req>
request)` |
+| `rpc A (stream Req) returns (stream Res)` | `ResponseStream<Res>
a(Stream<Req> request, {...})` | `Stream<Res> a(ServiceCall call, Stream<Req>
request)` |
+
+Server implementations use the generated streaming method shapes directly:
+
+```dart
+class GreeterService extends GreeterServiceBase {
+ @override
+ Stream<HelloReply> lotsOfReplies(
+ ServiceCall call,
+ HelloRequest request,
+ ) async* {
+ for (final greeting in ['Hello, ${request.name}', 'Welcome,
${request.name}']) {
+ yield HelloReply()..reply = greeting;
+ }
+ }
+
+ @override
+ Future<HelloReply> lotsOfGreetings(
+ ServiceCall call,
+ Stream<HelloRequest> request,
+ ) async {
+ final names = <String>[];
+ await for (final message in request) {
+ names.add(message.name);
+ }
+ return HelloReply()..reply = names.join(', ');
+ }
+
+ @override
+ Stream<HelloReply> chat(
+ ServiceCall call,
+ Stream<HelloRequest> request,
+ ) async* {
+ await for (final message in request) {
+ yield HelloReply()..reply = 'Hello, ${message.name}';
+ }
+ }
+}
+```
+
+Generated clients return the standard grpc-dart call objects:
+
+```dart
+// Server streaming.
+await for (final reply in client.lotsOfReplies(HelloRequest()..name = 'Fory'))
{
+ print(reply.reply);
+}
+
+// Client streaming.
+final summary = await client.lotsOfGreetings(
+ Stream.fromIterable([
+ HelloRequest()..name = 'Ada',
+ HelloRequest()..name = 'Grace',
+ ]),
+);
+print(summary.reply);
+
+// Bidirectional streaming.
+await for (final reply in client.chat(
+ Stream.fromIterable([HelloRequest()..name = 'Fory']),
+)) {
+ print(reply.reply);
+}
+```
+
+The generated descriptors preserve the exact IDL service and method names for
+the gRPC path, while the Dart methods use camelCase names.
+
+## Generated Module Names
+
+Dart model files and schema modules are named after the package's last segment,
+not the gRPC service name. (When a schema has no package, the source file stem
is
+used instead.)
+
+| Schema input (package) | Model file | Schema module
|
+| ------------------------------- | ------------------- |
----------------------- |
+| `service.fdl` (`demo.greeter`) | `greeter.dart` | `GreeterForyModule`
|
+| `api.fdl` (`demo.order_events`) | `order_events.dart` |
`OrderEventsForyModule` |
+| `greeter.fdl` (`demo.greeter`) | `greeter.dart` | `GreeterForyModule`
|
+
+A gRPC service named `Greeter` still generates the companion
+`<stem>_grpc.dart` with `GreeterClient` and `GreeterServiceBase`; it does not
+change the schema module name. If several schema files use the same package
+leaf, place them in distinct output directories or choose package/file names
that
+produce distinct Dart model files.
+
+## Operations
+
+The generated service code only replaces request and response serialization.
+All normal gRPC operational features still belong to your gRPC stack:
+
+- Deadlines and cancellations
+- TLS and authentication
+- Name resolution and load balancing
+- Client and server interceptors
+- Status codes and metadata
+- Channel lifecycle management
+
+## Troubleshooting
+
+### Missing `package:grpc` Types
+
+Add `grpc` to your application dependencies. Generated Fory service files
import
+grpc-dart APIs, but `fory` intentionally does not depend on gRPC.
+
+### Generated Code References a Missing `.fory.dart` Part
+
+Run `dart run build_runner build --delete-conflicting-outputs` after generating
+or regenerating the Dart sources. The serializer part file is produced by
+`build_runner`, not by `foryc`.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or expose a separate
protobuf
+service endpoint for generic protobuf clients.
diff --git a/docs/guide/dart/index.md b/docs/guide/dart/index.md
index 6c467bbe1..d779afc99 100644
--- a/docs/guide/dart/index.md
+++ b/docs/guide/dart/index.md
@@ -138,6 +138,7 @@ dart run build_runner build --delete-conflicting-outputs
| [Supported Types](supported-types.md) | Built-in xlang values,
wrappers, collections, and structs |
| [Schema Evolution](schema-evolution.md) | Compatible structs and
evolving schemas |
| [Web Platform Support](web-platform-support.md) | Dart VM/AOT, Flutter, and
web support, limits, and validation |
+| [gRPC Support](grpc-support.md) | Generated Fory-backed gRPC
service companions |
| [Troubleshooting](troubleshooting.md) | Common errors,
diagnostics, and validation steps |
## Related Resources
diff --git a/docs/guide/dart/troubleshooting.md
b/docs/guide/dart/troubleshooting.md
index d94ac4e79..5f066920a 100644
--- a/docs/guide/dart/troubleshooting.md
+++ b/docs/guide/dart/troubleshooting.md
@@ -138,9 +138,26 @@ dart run build_runner build --delete-conflicting-outputs
dart test
```
+## Generated gRPC files cannot find `package:grpc` types
+
+**Cause**: gRPC packages are application dependencies. The `fory` package does
+not add gRPC as a hard dependency.
+
+**Fix**: Add `grpc` to your `pubspec.yaml` (and the `build_runner` dev
+dependency), then run `dart pub get`. See [gRPC Support](grpc-support.md).
+
+## A protobuf client cannot decode a Fory gRPC service
+
+**Cause**: Fory gRPC companions use gRPC transports with Fory-encoded message
+bodies, not protobuf wire encoding.
+
+**Fix**: Use a Fory-generated client for Fory-generated services, or expose a
+separate protobuf service endpoint for generic protobuf clients.
+
## Related Topics
- [Xlang Serialization](xlang-serialization.md)
- [Code Generation](code-generation.md)
- [Custom Serializers](custom-serializers.md)
- [Web Platform Support](web-platform-support.md)
+- [gRPC Support](grpc-support.md)
diff --git a/integration_tests/grpc_tests/dart/.gitignore
b/integration_tests/grpc_tests/dart/.gitignore
new file mode 100644
index 000000000..304132b85
--- /dev/null
+++ b/integration_tests/grpc_tests/dart/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.packages
+pubspec.lock
+lib/generated/
diff --git a/integration_tests/grpc_tests/dart/bin/interop.dart
b/integration_tests/grpc_tests/dart/bin/interop.dart
new file mode 100644
index 000000000..21bd4736c
--- /dev/null
+++ b/integration_tests/grpc_tests/dart/bin/interop.dart
@@ -0,0 +1,520 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 'dart:async';
+import 'dart:io';
+
+import 'package:grpc/grpc.dart';
+
+import 'package:fory_grpc_interop/generated/grpc_fdl/grpc_fdl.dart';
+import 'package:fory_grpc_interop/generated/grpc_fdl/grpc_fdl_grpc.dart';
+import 'package:fory_grpc_interop/generated/grpc_fbs/grpc_fbs.dart';
+import 'package:fory_grpc_interop/generated/grpc_fbs/grpc_fbs_grpc.dart';
+import 'package:fory_grpc_interop/generated/grpc_pb/grpc_pb.dart';
+import 'package:fory_grpc_interop/generated/grpc_pb/grpc_pb_grpc.dart';
+
+void _expect(Object? actual, Object? expected, String what) {
+ if (actual != expected) {
+ throw StateError(
+ 'mismatch [$what]\n actual: $actual\n expected: $expected',
+ );
+ }
+}
+
+void _expectList(List<Object?> actual, List<Object?> expected, String what) {
+ if (actual.length != expected.length) {
+ throw StateError(
+ 'length mismatch [$what]: ${actual.length} != ${expected.length}',
+ );
+ }
+ for (var i = 0; i < actual.length; i++) {
+ _expect(actual[i], expected[i], '$what[$i]');
+ }
+}
+
+GrpcFdlRequest _fdlRequest(String id, int count, String payload) {
+ return GrpcFdlRequest()
+ ..id = id
+ ..count = count
+ ..payload = payload;
+}
+
+GrpcFdlResponse _fdlResponse(GrpcFdlRequest request, String tag, int offset) {
+ return GrpcFdlResponse()
+ ..id = '$tag:${request.id}'
+ ..count = request.count + offset
+ ..payload = '$tag:${request.payload}';
+}
+
+GrpcFdlResponse _fdlAggregate(List<GrpcFdlRequest> requests) {
+ return GrpcFdlResponse()
+ ..id = 'client:${requests.map((e) => e.id).join('+')}'
+ ..count = requests.fold(0, (sum, e) => sum + e.count)
+ ..payload = 'client:${requests.map((e) => e.payload).join('+')}';
+}
+
+GrpcFdlUnion _fdlUnionRequest(GrpcFdlRequest request) =>
+ GrpcFdlUnion.request(request);
+
+GrpcFdlUnion _fdlUnionResponse(
+ GrpcFdlRequest request,
+ String tag,
+ int offset,
+) => GrpcFdlUnion.response(_fdlResponse(request, tag, offset));
+
+GrpcFdlUnion _fdlUnionAggregate(List<GrpcFdlRequest> requests) =>
+ GrpcFdlUnion.response(_fdlAggregate(requests));
+
+GrpcFdlRequest _fdlRequestFromUnion(GrpcFdlUnion union) => union.requestValue;
+
+GrpcFbsRequest _fbsRequest(String id, int count, String payload) {
+ return GrpcFbsRequest()
+ ..id = id
+ ..count = count
+ ..payload = payload;
+}
+
+GrpcFbsResponse _fbsResponse(GrpcFbsRequest request, String tag, int offset) {
+ return GrpcFbsResponse()
+ ..id = '$tag:${request.id}'
+ ..count = request.count + offset
+ ..payload = '$tag:${request.payload}';
+}
+
+GrpcFbsResponse _fbsAggregate(List<GrpcFbsRequest> requests) {
+ return GrpcFbsResponse()
+ ..id = 'client:${requests.map((e) => e.id).join('+')}'
+ ..count = requests.fold(0, (sum, e) => sum + e.count)
+ ..payload = 'client:${requests.map((e) => e.payload).join('+')}';
+}
+
+GrpcFbsUnion _fbsUnionRequest(GrpcFbsRequest request) =>
+ GrpcFbsUnion.grpcFbsRequest(request);
+
+GrpcFbsUnion _fbsUnionResponse(
+ GrpcFbsRequest request,
+ String tag,
+ int offset,
+) => GrpcFbsUnion.grpcFbsResponse(_fbsResponse(request, tag, offset));
+
+GrpcFbsUnion _fbsUnionAggregate(List<GrpcFbsRequest> requests) =>
+ GrpcFbsUnion.grpcFbsResponse(_fbsAggregate(requests));
+
+GrpcFbsRequest _fbsRequestFromUnion(GrpcFbsUnion union) =>
+ union.grpcFbsRequestValue;
+
+GrpcPbRequest _pbRequest(String id, int count, GrpcPbRequest_Payload payload) {
+ return GrpcPbRequest()
+ ..id = id
+ ..count = count
+ ..payload = payload;
+}
+
+GrpcPbResponse_Payload? _pbResponsePayload(
+ GrpcPbRequest_Payload? payload,
+ String tag,
+ int offset,
+) {
+ if (payload == null) return null;
+ if (payload.isText) {
+ return GrpcPbResponse_Payload.text('$tag:${payload.textValue}');
+ }
+ return GrpcPbResponse_Payload.number(payload.numberValue + offset);
+}
+
+GrpcPbResponse _pbResponse(GrpcPbRequest request, String tag, int offset) {
+ return GrpcPbResponse()
+ ..id = '$tag:${request.id}'
+ ..count = request.count + offset
+ ..payload = _pbResponsePayload(request.payload, tag, offset);
+}
+
+GrpcPbResponse _pbAggregate(List<GrpcPbRequest> requests) {
+ final ids = requests.map((e) => e.id).join('+');
+ return GrpcPbResponse()
+ ..id = 'client:$ids'
+ ..count = requests.fold(0, (sum, e) => sum + e.count)
+ ..payload = GrpcPbResponse_Payload.text('client:$ids');
+}
+
+class FdlService extends FdlGrpcServiceServiceBase {
+ @override
+ Future<GrpcFdlResponse> unaryMessage(ServiceCall c, GrpcFdlRequest r) async
=>
+ _fdlResponse(r, 'unary', 10);
+
+ @override
+ Stream<GrpcFdlResponse> serverStreamMessage(
+ ServiceCall c,
+ GrpcFdlRequest r,
+ ) async* {
+ for (var i = 0; i < 3; i++) {
+ yield _fdlResponse(r, 'server-$i', i);
+ }
+ }
+
+ @override
+ Future<GrpcFdlResponse> clientStreamMessage(
+ ServiceCall c,
+ Stream<GrpcFdlRequest> r,
+ ) async => _fdlAggregate(await r.toList());
+
+ @override
+ Stream<GrpcFdlResponse> bidiStreamMessage(
+ ServiceCall c,
+ Stream<GrpcFdlRequest> r,
+ ) async* {
+ var i = 0;
+ await for (final v in r) {
+ yield _fdlResponse(v, 'bidi-$i', i);
+ i++;
+ }
+ }
+
+ @override
+ Future<GrpcFdlUnion> unaryUnion(ServiceCall c, GrpcFdlUnion r) async =>
+ _fdlUnionResponse(_fdlRequestFromUnion(r), 'unary', 10);
+
+ @override
+ Stream<GrpcFdlUnion> serverStreamUnion(ServiceCall c, GrpcFdlUnion r) async*
{
+ final item = _fdlRequestFromUnion(r);
+ for (var i = 0; i < 3; i++) {
+ yield _fdlUnionResponse(item, 'server-$i', i);
+ }
+ }
+
+ @override
+ Future<GrpcFdlUnion> clientStreamUnion(
+ ServiceCall c,
+ Stream<GrpcFdlUnion> r,
+ ) async {
+ final requests = <GrpcFdlRequest>[];
+ await for (final item in r) {
+ requests.add(_fdlRequestFromUnion(item));
+ }
+ return _fdlUnionAggregate(requests);
+ }
+
+ @override
+ Stream<GrpcFdlUnion> bidiStreamUnion(
+ ServiceCall c,
+ Stream<GrpcFdlUnion> r,
+ ) async* {
+ var i = 0;
+ await for (final item in r) {
+ yield _fdlUnionResponse(_fdlRequestFromUnion(item), 'bidi-$i', i);
+ i++;
+ }
+ }
+}
+
+class FbsService extends FbsGrpcServiceServiceBase {
+ @override
+ Future<GrpcFbsResponse> unaryMessage(ServiceCall c, GrpcFbsRequest r) async
=>
+ _fbsResponse(r, 'unary', 10);
+
+ @override
+ Stream<GrpcFbsResponse> serverStreamMessage(
+ ServiceCall c,
+ GrpcFbsRequest r,
+ ) async* {
+ for (var i = 0; i < 3; i++) {
+ yield _fbsResponse(r, 'server-$i', i);
+ }
+ }
+
+ @override
+ Future<GrpcFbsResponse> clientStreamMessage(
+ ServiceCall c,
+ Stream<GrpcFbsRequest> r,
+ ) async => _fbsAggregate(await r.toList());
+
+ @override
+ Stream<GrpcFbsResponse> bidiStreamMessage(
+ ServiceCall c,
+ Stream<GrpcFbsRequest> r,
+ ) async* {
+ var i = 0;
+ await for (final v in r) {
+ yield _fbsResponse(v, 'bidi-$i', i);
+ i++;
+ }
+ }
+
+ @override
+ Future<GrpcFbsUnion> unaryUnion(ServiceCall c, GrpcFbsUnion r) async =>
+ _fbsUnionResponse(_fbsRequestFromUnion(r), 'unary', 10);
+
+ @override
+ Stream<GrpcFbsUnion> serverStreamUnion(ServiceCall c, GrpcFbsUnion r) async*
{
+ final item = _fbsRequestFromUnion(r);
+ for (var i = 0; i < 3; i++) {
+ yield _fbsUnionResponse(item, 'server-$i', i);
+ }
+ }
+
+ @override
+ Future<GrpcFbsUnion> clientStreamUnion(
+ ServiceCall c,
+ Stream<GrpcFbsUnion> r,
+ ) async {
+ final requests = <GrpcFbsRequest>[];
+ await for (final item in r) {
+ requests.add(_fbsRequestFromUnion(item));
+ }
+ return _fbsUnionAggregate(requests);
+ }
+
+ @override
+ Stream<GrpcFbsUnion> bidiStreamUnion(
+ ServiceCall c,
+ Stream<GrpcFbsUnion> r,
+ ) async* {
+ var i = 0;
+ await for (final item in r) {
+ yield _fbsUnionResponse(_fbsRequestFromUnion(item), 'bidi-$i', i);
+ i++;
+ }
+ }
+}
+
+class PbService extends PbGrpcServiceServiceBase {
+ @override
+ Future<GrpcPbResponse> unaryMessage(ServiceCall c, GrpcPbRequest r) async =>
+ _pbResponse(r, 'unary', 10);
+
+ @override
+ Stream<GrpcPbResponse> serverStreamMessage(
+ ServiceCall c,
+ GrpcPbRequest r,
+ ) async* {
+ for (var i = 0; i < 3; i++) {
+ yield _pbResponse(r, 'server-$i', i);
+ }
+ }
+
+ @override
+ Future<GrpcPbResponse> clientStreamMessage(
+ ServiceCall c,
+ Stream<GrpcPbRequest> r,
+ ) async => _pbAggregate(await r.toList());
+
+ @override
+ Stream<GrpcPbResponse> bidiStreamMessage(
+ ServiceCall c,
+ Stream<GrpcPbRequest> r,
+ ) async* {
+ var i = 0;
+ await for (final v in r) {
+ yield _pbResponse(v, 'bidi-$i', i);
+ i++;
+ }
+ }
+}
+
+Future<void> _exerciseFdl(FdlGrpcServiceClient stub) async {
+ final messages = [
+ _fdlRequest('fdl-a', 1, 'alpha'),
+ _fdlRequest('fdl-b', 2, 'beta'),
+ ];
+ final first = messages[0];
+ _expect(
+ await stub.unaryMessage(first),
+ _fdlResponse(first, 'unary', 10),
+ 'fdl.unaryMessage',
+ );
+ _expectList(await stub.serverStreamMessage(first).toList(), [
+ for (var i = 0; i < 3; i++) _fdlResponse(first, 'server-$i', i),
+ ], 'fdl.serverStreamMessage');
+ _expect(
+ await stub.clientStreamMessage(Stream.fromIterable(messages)),
+ _fdlAggregate(messages),
+ 'fdl.clientStreamMessage',
+ );
+ _expectList(
+ await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+ [
+ for (var i = 0; i < messages.length; i++)
+ _fdlResponse(messages[i], 'bidi-$i', i),
+ ],
+ 'fdl.bidiStreamMessage',
+ );
+
+ final unionReqs = [
+ _fdlRequest('fdl-u-a', 3, 'union-alpha'),
+ _fdlRequest('fdl-u-b', 4, 'union-beta'),
+ ];
+ final unions = [for (final r in unionReqs) _fdlUnionRequest(r)];
+ final unionFirst = unionReqs[0];
+ _expect(
+ await stub.unaryUnion(unions[0]),
+ _fdlUnionResponse(unionFirst, 'unary', 10),
+ 'fdl.unaryUnion',
+ );
+ _expectList(await stub.serverStreamUnion(unions[0]).toList(), [
+ for (var i = 0; i < 3; i++) _fdlUnionResponse(unionFirst, 'server-$i', i),
+ ], 'fdl.serverStreamUnion');
+ _expect(
+ await stub.clientStreamUnion(Stream.fromIterable(unions)),
+ _fdlUnionAggregate(unionReqs),
+ 'fdl.clientStreamUnion',
+ );
+ _expectList(
+ await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(),
+ [
+ for (var i = 0; i < unionReqs.length; i++)
+ _fdlUnionResponse(unionReqs[i], 'bidi-$i', i),
+ ],
+ 'fdl.bidiStreamUnion',
+ );
+}
+
+Future<void> _exerciseFbs(FbsGrpcServiceClient stub) async {
+ final messages = [
+ _fbsRequest('fbs-a', 5, 'alpha'),
+ _fbsRequest('fbs-b', 6, 'beta'),
+ ];
+ final first = messages[0];
+ _expect(
+ await stub.unaryMessage(first),
+ _fbsResponse(first, 'unary', 10),
+ 'fbs.unaryMessage',
+ );
+ _expectList(await stub.serverStreamMessage(first).toList(), [
+ for (var i = 0; i < 3; i++) _fbsResponse(first, 'server-$i', i),
+ ], 'fbs.serverStreamMessage');
+ _expect(
+ await stub.clientStreamMessage(Stream.fromIterable(messages)),
+ _fbsAggregate(messages),
+ 'fbs.clientStreamMessage',
+ );
+ _expectList(
+ await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+ [
+ for (var i = 0; i < messages.length; i++)
+ _fbsResponse(messages[i], 'bidi-$i', i),
+ ],
+ 'fbs.bidiStreamMessage',
+ );
+
+ final unionReqs = [
+ _fbsRequest('fbs-u-a', 7, 'union-alpha'),
+ _fbsRequest('fbs-u-b', 8, 'union-beta'),
+ ];
+ final unions = [for (final r in unionReqs) _fbsUnionRequest(r)];
+ final unionFirst = unionReqs[0];
+ _expect(
+ await stub.unaryUnion(unions[0]),
+ _fbsUnionResponse(unionFirst, 'unary', 10),
+ 'fbs.unaryUnion',
+ );
+ _expectList(await stub.serverStreamUnion(unions[0]).toList(), [
+ for (var i = 0; i < 3; i++) _fbsUnionResponse(unionFirst, 'server-$i', i),
+ ], 'fbs.serverStreamUnion');
+ _expect(
+ await stub.clientStreamUnion(Stream.fromIterable(unions)),
+ _fbsUnionAggregate(unionReqs),
+ 'fbs.clientStreamUnion',
+ );
+ _expectList(
+ await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(),
+ [
+ for (var i = 0; i < unionReqs.length; i++)
+ _fbsUnionResponse(unionReqs[i], 'bidi-$i', i),
+ ],
+ 'fbs.bidiStreamUnion',
+ );
+}
+
+Future<void> _exercisePb(PbGrpcServiceClient stub) async {
+ final messages = [
+ _pbRequest('pb-a', 9, GrpcPbRequest_Payload.text('alpha')),
+ _pbRequest('pb-b', 10, GrpcPbRequest_Payload.number(42)),
+ ];
+ final first = messages[0];
+ _expect(
+ await stub.unaryMessage(first),
+ _pbResponse(first, 'unary', 10),
+ 'pb.unaryMessage',
+ );
+ _expectList(await stub.serverStreamMessage(first).toList(), [
+ for (var i = 0; i < 3; i++) _pbResponse(first, 'server-$i', i),
+ ], 'pb.serverStreamMessage');
+ _expect(
+ await stub.clientStreamMessage(Stream.fromIterable(messages)),
+ _pbAggregate(messages),
+ 'pb.clientStreamMessage',
+ );
+ _expectList(
+ await stub.bidiStreamMessage(Stream.fromIterable(messages)).toList(),
+ [
+ for (var i = 0; i < messages.length; i++)
+ _pbResponse(messages[i], 'bidi-$i', i),
+ ],
+ 'pb.bidiStreamMessage',
+ );
+}
+
+Future<void> _runClient(String target) async {
+ final parts = target.split(':');
+ final channel = ClientChannel(
+ parts[0],
+ port: int.parse(parts[1]),
+ options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
+ );
+ try {
+ await _exerciseFdl(FdlGrpcServiceClient(channel));
+ await _exerciseFbs(FbsGrpcServiceClient(channel));
+ await _exercisePb(PbGrpcServiceClient(channel));
+ } finally {
+ await channel.shutdown();
+ }
+}
+
+Future<void> _runServer(String portFilePath) async {
+ final server = Server.create(
+ services: [FdlService(), FbsService(), PbService()],
+ );
+ await server.serve(address: InternetAddress.loopbackIPv4, port: 0);
+ await File(portFilePath).writeAsString('${server.port!}', flush: true);
+ await Completer<void>().future;
+}
+
+String _flag(List<String> args, String name) {
+ final i = args.indexOf(name);
+ if (i < 0 || i + 1 >= args.length) {
+ throw ArgumentError('missing $name');
+ }
+ return args[i + 1];
+}
+
+Future<void> main(List<String> args) async {
+ try {
+ if (args.isNotEmpty && args[0] == 'client') {
+ await _runClient(_flag(args, '--target'));
+ } else if (args.isNotEmpty && args[0] == 'server') {
+ await _runServer(_flag(args, '--port-file'));
+ } else {
+ stderr.writeln(
+ 'usage: interop.dart <client --target H:P | server --port-file PATH>',
+ );
+ exit(2);
+ }
+ } catch (e, st) {
+ stderr.writeln('interop peer failed: $e\n$st');
+ exit(1);
+ }
+}
diff --git a/integration_tests/grpc_tests/run_tests.sh
b/integration_tests/grpc_tests/dart/pubspec.yaml
old mode 100755
new mode 100644
similarity index 53%
copy from integration_tests/grpc_tests/run_tests.sh
copy to integration_tests/grpc_tests/dart/pubspec.yaml
index 34fdcc59b..14e21dbf7
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/dart/pubspec.yaml
@@ -1,5 +1,3 @@
-#!/bin/bash
-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -17,23 +15,18 @@
# specific language governing permissions and limitations
# under the License.
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest}"
+name: fory_grpc_interop
+description: Apache Fory Java/Dart gRPC interop peer
+publish_to: none
+version: 0.0.0
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
+environment:
+ sdk: ^3.7.0
-python "${SCRIPT_DIR}/generate_grpc.py"
+dependencies:
+ grpc: ^4.0.0
+ fory:
+ path: ../../../dart/packages/fory
-cd "${SCRIPT_DIR}/go"
-go build -o grpc-interop .
-cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace --quiet
-cd "${SCRIPT_DIR}/kotlin"
-mvn --no-transfer-progress -DskipTests package
-cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
-mvn -T16 --no-transfer-progress \
- -Dtest="${TEST_CLASSES}" \
- test
+dev_dependencies:
+ build_runner: ">=2.7.0 <3.0.0"
diff --git a/integration_tests/grpc_tests/generate_grpc.py
b/integration_tests/grpc_tests/generate_grpc.py
index e89622b5c..f2950c1fe 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -38,6 +38,7 @@ OUTPUTS = {
"rust": TEST_DIR / "rust/generated/src",
"csharp": TEST_DIR / "csharp/generated",
"kotlin": TEST_DIR / "kotlin/src/main/kotlin/generated",
+ "dart": TEST_DIR / "dart/lib/generated",
}
@@ -81,6 +82,7 @@ def main() -> int:
f"--rust_out={OUTPUTS['rust']}",
f"--csharp_out={OUTPUTS['csharp']}",
f"--kotlin_out={OUTPUTS['kotlin']}",
+ f"--dart_out={OUTPUTS['dart']}",
"--grpc",
],
env=env,
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
similarity index 55%
copy from
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
copy to
integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
index c764a5f9e..743595d23 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java
@@ -20,16 +20,21 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
-public class RustGrpcTest extends GrpcTestBase {
+public class DartGrpcTest extends GrpcTestBase {
@Test
- public void testJavaServerRustClient() throws Exception {
+ public void testJavaServerDartClient() throws Exception {
Server server = startJavaAllSchemasServer();
try {
- runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ runPeer(
+ "dart-grpc-client", dartCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -37,8 +42,22 @@ public class RustGrpcTest extends GrpcTestBase {
}
@Test
- public void testJavaClientRustServer() throws Exception {
+ public void testDartServerJavaClient() throws Exception {
exercisePeerServer(
- "rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"),
this::exerciseAllSchemas);
+ "dart-grpc", "Dart", "fory-grpc-dart-", dartCommand("server"),
this::exerciseAllSchemas);
+ }
+
+ private PeerCommand dartCommand(String... args) {
+ Path dartRoot = grpcRoot().resolve("dart");
+ List<String> command = new ArrayList<>();
+ command.add("dart");
+ command.add("run");
+ command.add("bin/interop.dart");
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(dartRoot, command);
+ putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+ setLocalhostNoProxy(peerCommand);
+ clearProxyEnv(peerCommand);
+ return peerCommand;
}
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
index 8bcb3d5ef..fed51db84 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GoGrpcTest.java
@@ -20,6 +20,10 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
@@ -29,7 +33,7 @@ public class GoGrpcTest extends GrpcTestBase {
public void testJavaServerGoClient() throws Exception {
Server server = startJavaFdlServer();
try {
- runGo("go-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ runPeer("go-grpc-client", goCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -40,4 +44,14 @@ public class GoGrpcTest extends GrpcTestBase {
public void testGoServerJavaClient() throws Exception {
exercisePeerServer("go-grpc", "Go", "fory-grpc-go-", goCommand("server"),
this::exerciseFdl);
}
+
+ private PeerCommand goCommand(String... args) {
+ Path goRoot = grpcRoot().resolve("go");
+ List<String> command = new ArrayList<>();
+ command.add(goRoot.resolve("grpc-interop").toString());
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(goRoot, command);
+ setLocalhostNoProxy(peerCommand);
+ return peerCommand;
+ }
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
index cfec07151..9776493ff 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/GrpcTestBase.java
@@ -25,7 +25,6 @@ import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -274,155 +273,33 @@ public abstract class GrpcTestBase {
pbResponse(requests.get(0), "bidi-0", 0),
pbResponse(requests.get(1), "bidi-1", 1)));
}
- protected PeerCommand goCommand(String... args) {
- Path grpcRoot =
repoRoot().resolve("integration_tests").resolve("grpc_tests");
- Path goRoot = grpcRoot.resolve("go");
- List<String> command = new ArrayList<>();
- command.add(goRoot.resolve("grpc-interop").toString());
- command.addAll(Arrays.asList(args));
+ protected PeerCommand newPeerCommand(Path workDir, List<String> command) {
PeerCommand peerCommand = new PeerCommand();
peerCommand.command = command;
- peerCommand.workDir = goRoot;
- peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
- peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
+ peerCommand.workDir = workDir;
return peerCommand;
}
- protected void runGo(String peer, String... args) throws IOException,
InterruptedException {
- Process process = startPeer(goCommand(args));
- PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
- outputCollector.start();
- boolean finished = process.waitFor(180, TimeUnit.SECONDS);
- if (!finished) {
- process.destroyForcibly();
- process.waitFor(10, TimeUnit.SECONDS);
- Assert.fail("Peer process timed out for " + peer +
peerOutput(outputCollector));
- }
- int exitCode = process.exitValue();
- if (exitCode != 0) {
- Assert.fail(
- "Peer process failed for "
- + peer
- + " with exit code "
- + exitCode
- + peerOutput(outputCollector));
- }
- outputCollector.awaitOutput();
+ protected void putEnv(PeerCommand command, String key, String value) {
+ command.environment.put(key, value);
}
- protected PeerCommand pythonCommand(String... args) {
- Path pythonRoot =
-
repoRoot().resolve("integration_tests").resolve("grpc_tests").resolve("python");
- return pythonCommand(
- "grpc_tests.grpc_peer",
pythonRoot.resolve("grpc_tests").resolve("generated"), args);
+ protected void setLocalhostNoProxy(PeerCommand command) {
+ command.environment.put("NO_PROXY", "127.0.0.1,localhost");
+ command.environment.put("no_proxy", "127.0.0.1,localhost");
}
- protected PeerCommand pythonSyncCommand(String... args) {
- Path pythonRoot =
-
repoRoot().resolve("integration_tests").resolve("grpc_tests").resolve("python");
- return pythonCommand(
- "grpc_sync_tests.grpc_peer",
- pythonRoot.resolve("grpc_sync_tests").resolve("generated"),
- args);
- }
-
- private PeerCommand pythonCommand(String moduleName, Path generatedRoot,
String... args) {
- Path repoRoot = repoRoot();
- Path grpcRoot =
repoRoot.resolve("integration_tests").resolve("grpc_tests");
- Path pythonRoot = grpcRoot.resolve("python");
- String pythonPath =
- generatedRoot
- + File.pathSeparator
- + pythonRoot
- + File.pathSeparator
- + repoRoot.resolve("python");
- String existingPythonPath = System.getenv("PYTHONPATH");
- if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
- pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
- }
- List<String> command = new ArrayList<>();
- command.add("python");
- command.add("-m");
- command.add(moduleName);
- command.addAll(Arrays.asList(args));
- PeerCommand peerCommand = new PeerCommand();
- peerCommand.command = command;
- peerCommand.workDir = grpcRoot;
- peerCommand.environment.put("PYTHONPATH", pythonPath);
- peerCommand.environment.put("ENABLE_FORY_CYTHON_SERIALIZATION", "0");
- peerCommand.environment.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
- peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
- peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
- // Some developer and CI environments set proxy variables that grpcio
honors
- // even for localhost unless no_proxy is configured correctly.
+ protected void clearProxyEnv(PeerCommand command) {
+ // Some developer and CI environments set proxy variables that peer clients
+ // can honor even for localhost unless no_proxy is configured correctly.
for (String proxyVar :
Arrays.asList(
"all_proxy", "http_proxy", "https_proxy", "ALL_PROXY",
"HTTP_PROXY", "HTTPS_PROXY")) {
- peerCommand.environment.put(proxyVar, "");
+ command.environment.put(proxyVar, "");
}
- return peerCommand;
- }
-
- protected PeerCommand rustCommand(String... args) {
- Path repoRoot = repoRoot();
- Path grpcRoot =
repoRoot.resolve("integration_tests").resolve("grpc_tests");
- Path rustRoot = grpcRoot.resolve("rust");
- List<String> command = new ArrayList<>();
- command.add("cargo");
- command.add("run");
- command.add("--quiet");
- command.add("--manifest-path");
- command.add(rustRoot.resolve("interop").resolve("Cargo.toml").toString());
- command.add("--");
- command.addAll(Arrays.asList(args));
- PeerCommand peerCommand = new PeerCommand();
- peerCommand.command = command;
- peerCommand.workDir = rustRoot;
- peerCommand.environment.put("CARGO_TERM_COLOR", "never");
- peerCommand.environment.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
- peerCommand.environment.put("RUST_BACKTRACE", "1");
- peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
- peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
- for (String proxyVar :
- Arrays.asList(
- "all_proxy", "http_proxy", "https_proxy", "ALL_PROXY",
"HTTP_PROXY", "HTTPS_PROXY")) {
- peerCommand.environment.put(proxyVar, "");
- }
- return peerCommand;
- }
-
- protected PeerCommand kotlinCommand(String... args) {
- Path grpcRoot =
repoRoot().resolve("integration_tests").resolve("grpc_tests");
- Path kotlinRoot = grpcRoot.resolve("kotlin");
- List<String> command = new ArrayList<>();
- command.add("java");
- command.add("-jar");
-
command.add(kotlinRoot.resolve("target").resolve("fory-kotlin-grpc-peer.jar").toString());
- command.addAll(Arrays.asList(args));
- PeerCommand peerCommand = new PeerCommand();
- peerCommand.command = command;
- peerCommand.workDir = kotlinRoot;
- peerCommand.environment.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
- peerCommand.environment.put("NO_PROXY", "127.0.0.1,localhost");
- peerCommand.environment.put("no_proxy", "127.0.0.1,localhost");
- for (String proxyVar :
- Arrays.asList(
- "all_proxy", "http_proxy", "https_proxy", "ALL_PROXY",
"HTTP_PROXY", "HTTPS_PROXY")) {
- peerCommand.environment.put(proxyVar, "");
- }
- return peerCommand;
}
- protected void runPython(String peer, String... args) throws IOException,
InterruptedException {
- runPythonPeer(peer, pythonCommand(args));
- }
-
- protected void runPythonSync(String peer, String... args)
- throws IOException, InterruptedException {
- runPythonPeer(peer, pythonSyncCommand(args));
- }
-
- private void runPythonPeer(String peer, PeerCommand command)
+ protected void runPeer(String peer, PeerCommand command)
throws IOException, InterruptedException {
Process process = startPeer(command);
PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
@@ -445,50 +322,6 @@ public abstract class GrpcTestBase {
outputCollector.awaitOutput();
}
- protected void runRust(String peer, String... args) throws IOException,
InterruptedException {
- Process process = startPeer(rustCommand(args));
- PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
- outputCollector.start();
- boolean finished = process.waitFor(180, TimeUnit.SECONDS);
- if (!finished) {
- process.destroyForcibly();
- process.waitFor(10, TimeUnit.SECONDS);
- Assert.fail("Peer process timed out for " + peer +
peerOutput(outputCollector));
- }
- int exitCode = process.exitValue();
- if (exitCode != 0) {
- Assert.fail(
- "Peer process failed for "
- + peer
- + " with exit code "
- + exitCode
- + peerOutput(outputCollector));
- }
- outputCollector.awaitOutput();
- }
-
- protected void runKotlin(String peer, String... args) throws IOException,
InterruptedException {
- Process process = startPeer(kotlinCommand(args));
- PeerOutputCollector outputCollector = new
PeerOutputCollector(process.getInputStream(), peer);
- outputCollector.start();
- boolean finished = process.waitFor(180, TimeUnit.SECONDS);
- if (!finished) {
- process.destroyForcibly();
- process.waitFor(10, TimeUnit.SECONDS);
- Assert.fail("Peer process timed out for " + peer +
peerOutput(outputCollector));
- }
- int exitCode = process.exitValue();
- if (exitCode != 0) {
- Assert.fail(
- "Peer process failed for "
- + peer
- + " with exit code "
- + exitCode
- + peerOutput(outputCollector));
- }
- outputCollector.awaitOutput();
- }
-
private Process startPeer(PeerCommand command) throws IOException {
ProcessBuilder builder = new ProcessBuilder(command.command);
builder.redirectErrorStream(true);
@@ -526,7 +359,11 @@ public abstract class GrpcTestBase {
return output.isEmpty() ? "" : "\noutput:\n" + output;
}
- private Path repoRoot() {
+ protected Path grpcRoot() {
+ return repoRoot().resolve("integration_tests").resolve("grpc_tests");
+ }
+
+ protected Path repoRoot() {
Path moduleDir = java.nio.file.Paths.get("").toAbsolutePath();
return moduleDir.getParent().getParent().getParent();
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
index 3a84e693c..19630dbc5 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/KotlinGrpcTest.java
@@ -20,6 +20,10 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
@@ -29,7 +33,9 @@ public class KotlinGrpcTest extends GrpcTestBase {
public void testJavaServerKotlinClient() throws Exception {
Server server = startJavaAllSchemasServer();
try {
- runKotlin("kotlin-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ runPeer(
+ "kotlin-grpc-client",
+ kotlinCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -45,4 +51,18 @@ public class KotlinGrpcTest extends GrpcTestBase {
kotlinCommand("server"),
this::exerciseAllSchemas);
}
+
+ private PeerCommand kotlinCommand(String... args) {
+ Path kotlinRoot = grpcRoot().resolve("kotlin");
+ List<String> command = new ArrayList<>();
+ command.add("java");
+ command.add("-jar");
+
command.add(kotlinRoot.resolve("target").resolve("fory-kotlin-grpc-peer.jar").toString());
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(kotlinRoot, command);
+ putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+ setLocalhostNoProxy(peerCommand);
+ clearProxyEnv(peerCommand);
+ return peerCommand;
+ }
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
index b0e47838b..25ebb20f2 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonAsyncGrpcTest.java
@@ -20,6 +20,11 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
@@ -29,7 +34,9 @@ public class PythonAsyncGrpcTest extends GrpcTestBase {
public void testJavaServerPythonClient() throws Exception {
Server server = startJavaAllSchemasServer();
try {
- runPython("python-async-grpc-client", "client", "--target", "127.0.0.1:"
+ server.getPort());
+ runPeer(
+ "python-async-grpc-client",
+ pythonCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -45,4 +52,33 @@ public class PythonAsyncGrpcTest extends GrpcTestBase {
pythonCommand("server"),
this::exerciseAllSchemas);
}
+
+ private PeerCommand pythonCommand(String... args) {
+ Path repoRoot = repoRoot();
+ Path grpcRoot = grpcRoot();
+ Path pythonRoot = grpcRoot.resolve("python");
+ Path generatedRoot = pythonRoot.resolve("grpc_tests").resolve("generated");
+ String pythonPath =
+ generatedRoot
+ + File.pathSeparator
+ + pythonRoot
+ + File.pathSeparator
+ + repoRoot.resolve("python");
+ String existingPythonPath = System.getenv("PYTHONPATH");
+ if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
+ pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
+ }
+ List<String> command = new ArrayList<>();
+ command.add("python");
+ command.add("-m");
+ command.add("grpc_tests.grpc_peer");
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(grpcRoot, command);
+ putEnv(peerCommand, "PYTHONPATH", pythonPath);
+ putEnv(peerCommand, "ENABLE_FORY_CYTHON_SERIALIZATION", "0");
+ putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+ setLocalhostNoProxy(peerCommand);
+ clearProxyEnv(peerCommand);
+ return peerCommand;
+ }
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
index 3ea572c21..6870f4b8c 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/PythonSyncGrpcTest.java
@@ -20,6 +20,11 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
@@ -29,8 +34,9 @@ public class PythonSyncGrpcTest extends GrpcTestBase {
public void testJavaServerPythonClient() throws Exception {
Server server = startJavaAllSchemasServer();
try {
- runPythonSync(
- "python-sync-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ runPeer(
+ "python-sync-grpc-client",
+ pythonCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -43,7 +49,36 @@ public class PythonSyncGrpcTest extends GrpcTestBase {
"python-sync-grpc",
"Python",
"fory-grpc-python-sync-",
- pythonSyncCommand("server"),
+ pythonCommand("server"),
this::exerciseAllSchemas);
}
+
+ private PeerCommand pythonCommand(String... args) {
+ Path repoRoot = repoRoot();
+ Path grpcRoot = grpcRoot();
+ Path pythonRoot = grpcRoot.resolve("python");
+ Path generatedRoot =
pythonRoot.resolve("grpc_sync_tests").resolve("generated");
+ String pythonPath =
+ generatedRoot
+ + File.pathSeparator
+ + pythonRoot
+ + File.pathSeparator
+ + repoRoot.resolve("python");
+ String existingPythonPath = System.getenv("PYTHONPATH");
+ if (existingPythonPath != null && !existingPythonPath.isEmpty()) {
+ pythonPath = pythonPath + File.pathSeparator + existingPythonPath;
+ }
+ List<String> command = new ArrayList<>();
+ command.add("python");
+ command.add("-m");
+ command.add("grpc_sync_tests.grpc_peer");
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(grpcRoot, command);
+ putEnv(peerCommand, "PYTHONPATH", pythonPath);
+ putEnv(peerCommand, "ENABLE_FORY_CYTHON_SERIALIZATION", "0");
+ putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+ setLocalhostNoProxy(peerCommand);
+ clearProxyEnv(peerCommand);
+ return peerCommand;
+ }
}
diff --git
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
index c764a5f9e..1db867290 100644
---
a/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
+++
b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/RustGrpcTest.java
@@ -20,6 +20,10 @@
package org.apache.fory.grpc_tests;
import io.grpc.Server;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
@@ -29,7 +33,8 @@ public class RustGrpcTest extends GrpcTestBase {
public void testJavaServerRustClient() throws Exception {
Server server = startJavaAllSchemasServer();
try {
- runRust("rust-grpc-client", "client", "--target", "127.0.0.1:" +
server.getPort());
+ runPeer(
+ "rust-grpc-client", rustCommand("client", "--target", "127.0.0.1:" +
server.getPort()));
} finally {
server.shutdownNow();
server.awaitTermination(10, TimeUnit.SECONDS);
@@ -41,4 +46,23 @@ public class RustGrpcTest extends GrpcTestBase {
exercisePeerServer(
"rust-grpc", "Rust", "fory-grpc-rust-", rustCommand("server"),
this::exerciseAllSchemas);
}
+
+ private PeerCommand rustCommand(String... args) {
+ Path rustRoot = grpcRoot().resolve("rust");
+ List<String> command = new ArrayList<>();
+ command.add("cargo");
+ command.add("run");
+ command.add("--quiet");
+ command.add("--manifest-path");
+ command.add(rustRoot.resolve("interop").resolve("Cargo.toml").toString());
+ command.add("--");
+ command.addAll(Arrays.asList(args));
+ PeerCommand peerCommand = newPeerCommand(rustRoot, command);
+ putEnv(peerCommand, "CARGO_TERM_COLOR", "never");
+ putEnv(peerCommand, "ENABLE_FORY_DEBUG_OUTPUT", "1");
+ putEnv(peerCommand, "RUST_BACKTRACE", "1");
+ setLocalhostNoProxy(peerCommand);
+ clearProxyEnv(peerCommand);
+ return peerCommand;
+ }
}
diff --git a/integration_tests/grpc_tests/run_tests.sh
b/integration_tests/grpc_tests/run_tests.sh
index 34fdcc59b..784f2f05e 100755
--- a/integration_tests/grpc_tests/run_tests.sh
+++ b/integration_tests/grpc_tests/run_tests.sh
@@ -21,18 +21,37 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest}"
+TEST_CLASSES="${1:-PythonAsyncGrpcTest,PythonSyncGrpcTest,RustGrpcTest,GoGrpcTest,KotlinGrpcTest,DartGrpcTest}"
-python -m pip install "grpcio>=1.62.2,<1.71"
-python -m pip install -v -e "${ROOT_DIR}/python"
+has_test_class() {
+ [[ ",${TEST_CLASSES}," == *",$1,"* ]]
+}
+
+if has_test_class "PythonAsyncGrpcTest" || has_test_class
"PythonSyncGrpcTest"; then
+ python -m pip install "grpcio>=1.62.2,<1.71"
+ python -m pip install -v -e "${ROOT_DIR}/python"
+fi
python "${SCRIPT_DIR}/generate_grpc.py"
-cd "${SCRIPT_DIR}/go"
-go build -o grpc-interop .
-cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace --quiet
-cd "${SCRIPT_DIR}/kotlin"
-mvn --no-transfer-progress -DskipTests package
+if has_test_class "GoGrpcTest"; then
+ cd "${SCRIPT_DIR}/go"
+ go build -o grpc-interop .
+fi
+if has_test_class "RustGrpcTest"; then
+ cargo build --manifest-path "${SCRIPT_DIR}/rust/Cargo.toml" --workspace
--quiet
+fi
+if has_test_class "KotlinGrpcTest"; then
+ cd "${SCRIPT_DIR}/kotlin"
+ mvn --no-transfer-progress -DskipTests package
+fi
+if has_test_class "DartGrpcTest"; then
+ cd "${SCRIPT_DIR}/dart"
+ dart pub get
+ dart run build_runner build
+ dart analyze bin lib/generated/*/*_grpc.dart
+ dart format --output=none --set-exit-if-changed bin
lib/generated/*/*_grpc.dart
+fi
cd "${ROOT_DIR}/integration_tests/grpc_tests/java"
mvn -T16 --no-transfer-progress \
-Dtest="${TEST_CLASSES}" \
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]