diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md index 616a4cc0d0..a68af06934 100644 --- a/.agents/languages/dart.md +++ b/.agents/languages/dart.md @@ -22,6 +22,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 (`_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 (`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 287b9212b6..065946cef0 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 557cd19b94..91a0ef7c53 100644 --- a/compiler/fory_compiler/generators/dart.py +++ b/compiler/fory_compiler/generators/dart.py @@ -23,6 +23,7 @@ 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.types import PrimitiveKind -class DartGenerator(BaseGenerator): +class DartGenerator(DartServiceGeneratorMixin, BaseGenerator): language_name = "dart" file_extension = ".dart" @@ -1291,8 +1292,10 @@ def generate_module_type(self, indent: int) -> List[str]: 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 0000000000..d875604d7a --- /dev/null +++ b/compiler/fory_compiler/generators/services/dart.py @@ -0,0 +1,335 @@ +# 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, 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"} + ) + + 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_payload_imports: Dict[str, Tuple[str, str]] = {} + + 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 _models;") + 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"_models.{self.module_type_name()}.getFory()" + lines.append("List _serialize(T value) =>") + lines.append(f" {fory}.serialize(value, trackRef: true);") + lines.append("") + lines.append("T _deserialize(List bytes) {") + lines.append( + " final u8 = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);" + ) + lines.append(f" return {fory}.deserialize(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"_models.{named_type.name}" + if self.is_imported_type(type_def): + schema = self._load_schema(type_def.location.file) + if schema is not None: + alias = self.safe_identifier( + schema.package.replace(".", "_") + if schema.package + else Path(type_def.location.file).stem + ) + self._grpc_payload_imports[type_def.location.file] = ( + self._relative_import_path(schema), + alias, + ) + return self.ref_name(type_def) + return f"_models.{self.local_name(type_def)}" + + 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. + 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 ;`, 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 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 for a single request and a + # Stream for a client-streaming request, and consumes the handler's + # return value directly as the response (a Future for a single + # response, a Stream 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 b7d2d2a972..9f47280343 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.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 @@ 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,350 @@ 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_service_codegen_uses_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" 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_and_fbs_service_codegen(): + 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(" in content + assert "_models.demo.common.Shared" not in content + + +def test_dart_grpc_rejects_reserved_method_names(): + 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 0c5ebd4912..89a2427b64 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 `_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 89b3751531..0ad11a6487 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 1f52db67e3..b3e5ebc91f 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 `_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 sayHello(HelloRequest request, {CallOptions? options}) { ... } + ResponseStream sayHellos(HelloRequest request, {CallOptions? options}) { ... } + ResponseFuture collectHellos(Stream request, {CallOptions? options}) { ... } + ResponseStream chatHellos(Stream request, {CallOptions? options}) { ... } +} + +abstract class GreeterServiceBase extends Service { + Future sayHello(ServiceCall call, HelloRequest request); + Stream sayHellos(ServiceCall call, HelloRequest request); + Future collectHellos(ServiceCall call, Stream request); + Stream chatHellos(ServiceCall call, Stream request); +} +``` + +A single-response client method returns `ResponseFuture` (client-streaming +adapts the streaming call with `.single`); a streaming-response method returns +`ResponseStream`. On the server, implementations override the abstract methods, +which receive a single request as `Q` and a client-streaming request as +`Stream`, 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 da10b2cd16..caa69a6fc9 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 da5deba311..d9355deece 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 d2e12c989e..633b90f1e0 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 0000000000..bc8fac968b --- /dev/null +++ b/docs/guide/dart/grpc-support.md @@ -0,0 +1,291 @@ +--- +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 sayHello(ServiceCall call, HelloRequest request) async { + final reply = HelloReply()..reply = 'Hello, ${request.name}'; + return reply; + } +} + +Future 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 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` (client-streaming adapts the call with `.single`); streaming +responses return a `ResponseStream`. 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 a(Req request, {CallOptions?})` | `Future a(ServiceCall call, Req request)` | +| `rpc A (Req) returns (stream Res)` | `ResponseStream a(Req request, {CallOptions?})` | `Stream a(ServiceCall call, Req request)` | +| `rpc A (stream Req) returns (Res)` | `ResponseFuture a(Stream request, {...})` | `Future a(ServiceCall call, Stream request)` | +| `rpc A (stream Req) returns (stream Res)` | `ResponseStream a(Stream request, {...})` | `Stream a(ServiceCall call, Stream request)` | + +Server implementations use the generated streaming method shapes directly: + +```dart +class GreeterService extends GreeterServiceBase { + @override + Stream lotsOfReplies( + ServiceCall call, + HelloRequest request, + ) async* { + for (final greeting in ['Hello, ${request.name}', 'Welcome, ${request.name}']) { + yield HelloReply()..reply = greeting; + } + } + + @override + Future lotsOfGreetings( + ServiceCall call, + Stream request, + ) async { + final names = []; + await for (final message in request) { + names.add(message.name); + } + return HelloReply()..reply = names.join(', '); + } + + @override + Stream chat( + ServiceCall call, + Stream 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 +`_grpc.dart` with `GreeterClient` and `GreeterServiceBase`; it does not +change the schema module name. This lets several schema files share the same +package without colliding. + +## 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 6c467bbe1c..d779afc99d 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 d94ac4e794..5f066920a2 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 0000000000..304132b856 --- /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 0000000000..21bd4736c5 --- /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 actual, List 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 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 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 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 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 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 unaryMessage(ServiceCall c, GrpcFdlRequest r) async => + _fdlResponse(r, 'unary', 10); + + @override + Stream serverStreamMessage( + ServiceCall c, + GrpcFdlRequest r, + ) async* { + for (var i = 0; i < 3; i++) { + yield _fdlResponse(r, 'server-$i', i); + } + } + + @override + Future clientStreamMessage( + ServiceCall c, + Stream r, + ) async => _fdlAggregate(await r.toList()); + + @override + Stream bidiStreamMessage( + ServiceCall c, + Stream r, + ) async* { + var i = 0; + await for (final v in r) { + yield _fdlResponse(v, 'bidi-$i', i); + i++; + } + } + + @override + Future unaryUnion(ServiceCall c, GrpcFdlUnion r) async => + _fdlUnionResponse(_fdlRequestFromUnion(r), 'unary', 10); + + @override + Stream 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 clientStreamUnion( + ServiceCall c, + Stream r, + ) async { + final requests = []; + await for (final item in r) { + requests.add(_fdlRequestFromUnion(item)); + } + return _fdlUnionAggregate(requests); + } + + @override + Stream bidiStreamUnion( + ServiceCall c, + Stream 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 unaryMessage(ServiceCall c, GrpcFbsRequest r) async => + _fbsResponse(r, 'unary', 10); + + @override + Stream serverStreamMessage( + ServiceCall c, + GrpcFbsRequest r, + ) async* { + for (var i = 0; i < 3; i++) { + yield _fbsResponse(r, 'server-$i', i); + } + } + + @override + Future clientStreamMessage( + ServiceCall c, + Stream r, + ) async => _fbsAggregate(await r.toList()); + + @override + Stream bidiStreamMessage( + ServiceCall c, + Stream r, + ) async* { + var i = 0; + await for (final v in r) { + yield _fbsResponse(v, 'bidi-$i', i); + i++; + } + } + + @override + Future unaryUnion(ServiceCall c, GrpcFbsUnion r) async => + _fbsUnionResponse(_fbsRequestFromUnion(r), 'unary', 10); + + @override + Stream 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 clientStreamUnion( + ServiceCall c, + Stream r, + ) async { + final requests = []; + await for (final item in r) { + requests.add(_fbsRequestFromUnion(item)); + } + return _fbsUnionAggregate(requests); + } + + @override + Stream bidiStreamUnion( + ServiceCall c, + Stream 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 unaryMessage(ServiceCall c, GrpcPbRequest r) async => + _pbResponse(r, 'unary', 10); + + @override + Stream serverStreamMessage( + ServiceCall c, + GrpcPbRequest r, + ) async* { + for (var i = 0; i < 3; i++) { + yield _pbResponse(r, 'server-$i', i); + } + } + + @override + Future clientStreamMessage( + ServiceCall c, + Stream r, + ) async => _pbAggregate(await r.toList()); + + @override + Stream bidiStreamMessage( + ServiceCall c, + Stream r, + ) async* { + var i = 0; + await for (final v in r) { + yield _pbResponse(v, 'bidi-$i', i); + i++; + } + } +} + +Future _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 _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 _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 _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 _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().future; +} + +String _flag(List args, String name) { + final i = args.indexOf(name); + if (i < 0 || i + 1 >= args.length) { + throw ArgumentError('missing $name'); + } + return args[i + 1]; +} + +Future main(List 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 ', + ); + exit(2); + } + } catch (e, st) { + stderr.writeln('interop peer failed: $e\n$st'); + exit(1); + } +} diff --git a/integration_tests/grpc_tests/dart/pubspec.yaml b/integration_tests/grpc_tests/dart/pubspec.yaml new file mode 100644 index 0000000000..14e21dbf75 --- /dev/null +++ b/integration_tests/grpc_tests/dart/pubspec.yaml @@ -0,0 +1,32 @@ +# 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. + +name: fory_grpc_interop +description: Apache Fory Java/Dart gRPC interop peer +publish_to: none +version: 0.0.0 + +environment: + sdk: ^3.7.0 + +dependencies: + grpc: ^4.0.0 + fory: + path: ../../../dart/packages/fory + +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 e89622b5cd..f2950c1fe5 100644 --- a/integration_tests/grpc_tests/generate_grpc.py +++ b/integration_tests/grpc_tests/generate_grpc.py @@ -38,6 +38,7 @@ "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/DartGrpcTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java new file mode 100644 index 0000000000..4d9ce055a0 --- /dev/null +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package org.apache.fory.grpc_tests; + +import io.grpc.Server; +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +public class DartGrpcTest extends GrpcTestBase { + + @Test + public void testJavaServerDartClient() throws Exception { + Server server = startJavaAllSchemasServer(); + try { + runDart("dart-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); + } finally { + server.shutdownNow(); + server.awaitTermination(10, TimeUnit.SECONDS); + } + } + + @Test + public void testDartServerJavaClient() throws Exception { + exercisePeerServer( + "dart-grpc", "Dart", "fory-grpc-dart-", dartCommand("server"), this::exerciseAllSchemas); + } +} 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 cfec07151b..042542b8d8 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 @@ -310,6 +310,50 @@ protected void runGo(String peer, String... args) throws IOException, Interrupte outputCollector.awaitOutput(); } + protected PeerCommand dartCommand(String... args) { + Path grpcRoot = repoRoot().resolve("integration_tests").resolve("grpc_tests"); + Path dartRoot = grpcRoot.resolve("dart"); + List command = new ArrayList<>(); + command.add("dart"); + command.add("run"); + command.add("bin/interop.dart"); + command.addAll(Arrays.asList(args)); + PeerCommand peerCommand = new PeerCommand(); + peerCommand.command = command; + peerCommand.workDir = dartRoot; + 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 runDart(String peer, String... args) throws IOException, InterruptedException { + Process process = startPeer(dartCommand(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 PeerCommand pythonCommand(String... args) { Path pythonRoot = repoRoot().resolve("integration_tests").resolve("grpc_tests").resolve("python"); diff --git a/integration_tests/grpc_tests/run_tests.sh b/integration_tests/grpc_tests/run_tests.sh index 34fdcc59b3..9057727d9d 100755 --- a/integration_tests/grpc_tests/run_tests.sh +++ b/integration_tests/grpc_tests/run_tests.sh @@ -21,7 +21,7 @@ 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" @@ -33,6 +33,11 @@ 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 "${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 cd "${ROOT_DIR}/integration_tests/grpc_tests/java" mvn -T16 --no-transfer-progress \ -Dtest="${TEST_CLASSES}" \