From d45a4afd481b58efb5488b6f22fe9e0cee08a068 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:13:16 +0530 Subject: [PATCH 01/18] feat(compiler): add dart gRPC streaming-rejection gate Wire DartServiceGeneratorMixin into DartGenerator via MRO matching JavaGenerator and PythonGenerator. The mixin currently exposes only the streaming-rejection check, which raises ValueError listing every offending method when an FDL service contains stream RPCs. File emission and Client/ServiceBase generation land in follow-up commits. --- compiler/fory_compiler/generators/dart.py | 3 +- .../fory_compiler/generators/services/dart.py | 53 +++++++++++++++++++ .../tests/test_service_codegen.py | 34 ++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 compiler/fory_compiler/generators/services/dart.py diff --git a/compiler/fory_compiler/generators/dart.py b/compiler/fory_compiler/generators/dart.py index 9ed3ad720c..d38100f19c 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" diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py new file mode 100644 index 0000000000..8612e2c792 --- /dev/null +++ b/compiler/fory_compiler/generators/services/dart.py @@ -0,0 +1,53 @@ +# 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 typing import List + +from fory_compiler.generators.base import GeneratedFile +from fory_compiler.ir.ast import RpcMethod, Service + + +class DartServiceGeneratorMixin: + """Generates Dart gRPC service companions (unary RPCs only).""" + + 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_streaming_unsupported(local_services) + + return [] + + def check_dart_streaming_unsupported(self, services: List[Service]) -> None: + offenders = [] + for service in services: + for method in service.methods: + if method.client_streaming or method.server_streaming: + offenders.append(f"{service.name}.{method.name}") + if offenders: + joined = "\n - " + "\n - ".join(offenders) + raise ValueError( + "Dart gRPC generator does not yet support streaming RPCs;\n" + "remove `stream` from the following methods or omit --grpc for dart:" + + joined + ) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 413fbf0131..6a1e897b24 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -219,6 +219,40 @@ def test_grpc_streaming_method_shapes(): assert "self.bidi = channel.stream_stream(" in python +def test_dart_grpc_rejects_streaming(): + from fory_compiler.generators.dart import DartGenerator + + schema = parse_fdl( + dedent( + """ + package demo.streams; + + message Req {} + message Res {} + + service Streamer { + rpc Unary (Req) returns (Res); + rpc Server (Req) returns (stream Res); + rpc Client (stream Req) returns (Res); + rpc Bidi (stream Req) returns (stream Res); + } + """ + ) + ) + + import pytest + + with pytest.raises(ValueError) as excinfo: + generate_service_files(schema, DartGenerator) + + msg = str(excinfo.value) + assert "Dart gRPC generator does not yet support streaming RPCs" in msg + assert "Streamer.Server" in msg + assert "Streamer.Client" in msg + assert "Streamer.Bidi" in msg + assert "Streamer.Unary" not in msg + + def test_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( From c60a6a36082a3a5568ef1b5c7010dc081c57f82a Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:15:47 +0530 Subject: [PATCH 02/18] feat(compiler): emit dart grpc skeleton with codec helpers Add generate_grpc_module to DartServiceGeneratorMixin: emits a _grpc.dart companion next to the messages file with Apache header, dart:async/dart:typed_data/package:grpc imports, a sibling import of the messages module as _models, and the two top-level codec helpers (_serialize and _deserialize) that route through the existing ForyRegistration.getFory() static. _deserialize includes the is-Uint8List guard for zero-copy on the common HTTP/2 path. --- .../fory_compiler/generators/services/dart.py | 41 ++++++++++++++++++- .../tests/test_service_codegen.py | 21 ++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index 8612e2c792..9ed1d5d655 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -17,6 +17,7 @@ """Dart gRPC service generator helpers.""" +from pathlib import Path from typing import List from fory_compiler.generators.base import GeneratedFile @@ -36,7 +37,7 @@ def generate_services(self) -> List[GeneratedFile]: return [] self.check_dart_streaming_unsupported(local_services) - return [] + return [self.generate_grpc_module(local_services)] def check_dart_streaming_unsupported(self, services: List[Service]) -> None: offenders = [] @@ -51,3 +52,41 @@ def check_dart_streaming_unsupported(self, services: List[Service]) -> None: "remove `stream` from the following methods or omit --grpc for dart:" + joined ) + + def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: + """Emit a grpc-dart companion module for schema services.""" + models_stem = Path(self.module_file_name()).stem # e.g. "demo_greeter" + grpc_path = f"{models_stem}_grpc.dart" + + 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;") + lines.append("") + lines.append( + "// grpc-dart Service self-registers via $methods; " + "no separate registration helper needed." + ) + lines.append("") + lines.append("List _serialize(T value) =>") + lines.append(" _models.ForyRegistration.getFory().serialize(value);") + lines.append("") + lines.append("T _deserialize(List bytes) {") + lines.append( + " final u8 = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);" + ) + lines.append(" return _models.ForyRegistration.getFory().deserialize(u8);") + lines.append("}") + lines.append("") + + return GeneratedFile(path=grpc_path, content="\n".join(lines)) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 6a1e897b24..43b73d6e4e 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -253,6 +253,27 @@ def test_dart_grpc_rejects_streaming(): assert "Streamer.Unary" not in msg +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_grpc.dart"} + content = files["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.ForyRegistration.getFory().serialize(value)" in content + assert "_models.ForyRegistration.getFory().deserialize" in content + assert "is Uint8List ? bytes : Uint8List.fromList(bytes)" in content + + + def test_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( From f7fd3aca5467c6ff5cdabed2f6154a60a375d746 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:18:39 +0530 Subject: [PATCH 03/18] feat(compiler): emit dart grpc Client class Emit per-service Client with ClientMethod constants and $createUnaryCall-backed unary methods. Constructor uses super-parameter syntax confirmed by grpc 4.3.1's in-tree TestClient(super.channel). Method names are normalized through safe_identifier(to_camel_case(method.name)). --- .../fory_compiler/generators/services/dart.py | 45 +++++++++++++++++++ .../tests/test_service_codegen.py | 10 +++++ 2 files changed, 55 insertions(+) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index 9ed1d5d655..b59d3480a0 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -89,4 +89,49 @@ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: lines.append("}") lines.append("") + for service in services: + lines.extend(self.generate_dart_grpc_client(service)) + lines.append("") + return GeneratedFile(path=grpc_path, content="\n".join(lines)) + + 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 = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + full_path = self.get_grpc_method_path(service, method) + lines.append( + f" static final {method_const} = " + 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: + method_const = f"_${self.dart_grpc_method_name(method)}" + req_t = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + method_name = self.dart_grpc_method_name(method) + lines.append("") + lines.append(f" ResponseFuture<{res_t}> {method_name}(") + lines.append(f" {req_t} request, {{") + lines.append(" CallOptions? options,") + lines.append(" }) {") + lines.append( + f" return $createUnaryCall({method_const}, request, options: options);" + ) + lines.append(" }") + 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 43b73d6e4e..88603c91eb 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -272,6 +272,16 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): assert "_models.ForyRegistration.getFory().deserialize" in content assert "is Uint8List ? bytes : Uint8List.fromList(bytes)" in content + assert "class GreeterClient extends Client {" in content + assert ( + "static final _$sayHello = " + "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 def test_java_outer_classname_service_references_nested_model_types(): From 32a6003492c8468c5d92a6461c10104ab66c3cb5 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:20:28 +0530 Subject: [PATCH 04/18] feat(compiler): emit dart grpc ServiceBase class Emit ServiceBase: registers each unary RPC via $addMethod in the constructor and exposes an abstract handler per method. Uses a _Pre shim that awaits the request Future before dispatching, since grpc-dart's ServiceMethod always hands the handler a Future --- .../fory_compiler/generators/services/dart.py | 53 +++++++++++++++++++ .../tests/test_service_codegen.py | 12 +++++ 2 files changed, 65 insertions(+) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index b59d3480a0..b523e8c5f9 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -92,6 +92,8 @@ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: for service in services: lines.extend(self.generate_dart_grpc_client(service)) lines.append("") + lines.extend(self.generate_dart_grpc_service_base(service)) + lines.append("") return GeneratedFile(path=grpc_path, content="\n".join(lines)) @@ -133,5 +135,56 @@ def generate_dart_grpc_client(self, service: Service) -> List[str]: lines.append("}") return lines + 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: + req_t = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + method_name = self.dart_grpc_method_name(method) + lines.append( + f" $addMethod(ServiceMethod<{req_t}, {res_t}>(" + ) + lines.append(f" '{method.name}',") + lines.append(f" {method_name}_Pre,") + lines.append(" false,") + lines.append(" false,") + lines.append( + f" (List value) => _deserialize<{req_t}>(value)," + ) + lines.append(f" ({res_t} value) => _serialize(value),") + lines.append(" ));") + lines.append(" }") + lines.append("") + for idx, method in enumerate(service.methods): + req_t = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + method_name = self.dart_grpc_method_name(method) + lines.append( + " // protoc_plugin parity: _Pre shim awaits the request future," + ) + lines.append(" // then delegates to the user-overridable method.") + lines.append(f" Future<{res_t}> {method_name}_Pre(") + lines.append(" ServiceCall $call,") + lines.append(f" Future<{req_t}> $request,") + lines.append(" ) async {") + lines.append(f" return {method_name}($call, await $request);") + lines.append(" }") + lines.append("") + lines.append(f" Future<{res_t}> {method_name}(") + lines.append(" ServiceCall call,") + lines.append(f" {req_t} request,") + lines.append(" );") + if idx != len(service.methods) - 1: + lines.append("") + 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 88603c91eb..87907a36f3 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -283,6 +283,18 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): 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(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_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( From ce9f651085376263f131870ec325e8a532858df3 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:21:32 +0530 Subject: [PATCH 05/18] feat(compiler): detect dart grpc class-name collisions Add check_dart_grpc_service_collisions to the mixin. For each local service, compute its emitted Client and ServiceBase class names and check against the set of top-level names DartGenerator would already emit for messages/enums/unions/registration. Also detect service<->service collisions when two services in the same schema would emit the same Client/ServiceBase name. Raises ValueError naming the offending class; the CLI's existing ValueError wrap at cli.py:686-690 prints it and exits non-zero. --- .../fory_compiler/generators/services/dart.py | 14 ++++++++- .../tests/test_service_codegen.py | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index b523e8c5f9..2a74833f32 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -36,7 +36,7 @@ def generate_services(self) -> List[GeneratedFile]: if not local_services: return [] self.check_dart_streaming_unsupported(local_services) - + self.check_dart_grpc_service_collisions(local_services) return [self.generate_grpc_module(local_services)] def check_dart_streaming_unsupported(self, services: List[Service]) -> None: @@ -53,6 +53,18 @@ def check_dart_streaming_unsupported(self, services: List[Service]) -> None: + joined ) + 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 generate_grpc_module(self, services: List[Service]) -> GeneratedFile: """Emit a grpc-dart companion module for schema services.""" models_stem = Path(self.module_file_name()).stem # e.g. "demo_greeter" diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 87907a36f3..9b45301a40 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -296,6 +296,37 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): 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_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( From d97f71ce97fa2ff56d61b8c866e0078dcc2c2d2d Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:22:38 +0530 Subject: [PATCH 06/18] feat(compiler): detect dart grpc method-name collisions Add check_dart_grpc_method_collisions. For each service, normalize every method name through safe_identifier(to_camel_case(name)) and raise ValueError if two distinct RpcMethod names collapse to the same Dart identifier (e.g. SayHello and say_hello both produce sayHello). --- .../fory_compiler/generators/services/dart.py | 13 ++++++++ .../tests/test_service_codegen.py | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index 2a74833f32..f4ad76ffd8 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -37,6 +37,7 @@ def generate_services(self) -> List[GeneratedFile]: return [] self.check_dart_streaming_unsupported(local_services) self.check_dart_grpc_service_collisions(local_services) + self.check_dart_grpc_method_collisions(local_services) return [self.generate_grpc_module(local_services)] def check_dart_streaming_unsupported(self, services: List[Service]) -> None: @@ -65,6 +66,18 @@ def check_dart_grpc_service_collisions(self, services: List[Service]) -> None: ) 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 generate_grpc_module(self, services: List[Service]) -> GeneratedFile: """Emit a grpc-dart companion module for schema services.""" models_stem = Path(self.module_file_name()).stem # e.g. "demo_greeter" diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 9b45301a40..fbc42ff830 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -327,6 +327,37 @@ def test_dart_grpc_service_class_collision(): assert "conflicts" in msg +def test_dart_grpc_method_collision(): + from fory_compiler.generators.dart import DartGenerator + + # `SayHello` and `say_hello` both normalize to camelCase `sayHello`. + 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_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( From e59a305eeebfcb1009c2eac066f44c1e65990e88 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:23:16 +0530 Subject: [PATCH 07/18] test(compiler): cover dart grpc codegen from proto and fbs frontends Mirror Python's test_proto_and_fbs_grpc_service_codegen: one test exercises a .proto-sourced service through ProtoTranslator and a .fbs-sourced service through FbsTranslator, then asserts both end up in the Dart gRPC emitter with the right method paths and Client classes. Confirms no Dart-specific translator wiring was needed. --- .../tests/test_service_codegen.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index fbc42ff830..a54f615b5c 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -358,6 +358,52 @@ def test_dart_grpc_method_collision(): 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_grpc.dart" in proto_dart + assert ( + "'/demo.proto.ProtoSvc/Call'," + in proto_dart["demo_proto_grpc.dart"] + ) + assert "class ProtoSvcClient extends Client {" in proto_dart["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_grpc.dart" in fbs_dart + assert "'/demo.fbs.FbsSvc/Call'," in fbs_dart["demo_fbs_grpc.dart"] + assert "class FbsSvcClient extends Client {" in fbs_dart["demo_fbs_grpc.dart"] + + def test_java_outer_classname_service_references_nested_model_types(): schema = parse_fdl( dedent( From 93b2e5e41bc5dfeb2f47f98e6866ddd101ca0bdc Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:25:14 +0530 Subject: [PATCH 08/18] test(compiler): include dart in parametric service-codegen tests Register DartGenerator in GENERATOR_CLASSES and add it to the gRPC-supported skip in the unsupported-generators test. --- compiler/fory_compiler/tests/test_service_codegen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index a54f615b5c..9a6ecb99cc 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -33,6 +33,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.python import PythonGenerator @@ -50,6 +51,7 @@ GoGenerator, CSharpGenerator, SwiftGenerator, + DartGenerator, ) _GREETER_WITH_SERVICE = dedent( @@ -129,7 +131,7 @@ def test_service_definition_does_not_affect_message_codegen(): def test_generate_services_returns_empty_list_for_unsupported_generators(): schema = parse_fdl(_GREETER_WITH_SERVICE) for generator_cls in GENERATOR_CLASSES: - if generator_cls in (JavaGenerator, PythonGenerator): + if generator_cls in (JavaGenerator, PythonGenerator, DartGenerator): continue options = GeneratorOptions(output_dir=Path("/tmp")) generator = generator_cls(schema, options) From 71c3d9eaaa4020d90a297f06a60b88bf6baf3732 Mon Sep 17 00:00:00 2001 From: yash Date: Sat, 30 May 2026 19:29:56 +0530 Subject: [PATCH 09/18] test(compiler): add opt-in dart analyze smoke test Emit _grpc.dart under the same package directory as the messages file. Add opt-in dart analyze gate covering the emitted file. --- .../fory_compiler/generators/services/dart.py | 5 +- .../tests/test_dart_grpc_analyze.py | 127 ++++++++++++++++++ .../tests/test_service_codegen.py | 16 +-- 3 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 compiler/fory_compiler/tests/test_dart_grpc_analyze.py diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index f4ad76ffd8..ad91f53354 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -80,8 +80,9 @@ def check_dart_grpc_method_collisions(self, services: List[Service]) -> None: def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: """Emit a grpc-dart companion module for schema services.""" - models_stem = Path(self.module_file_name()).stem # e.g. "demo_greeter" - grpc_path = f"{models_stem}_grpc.dart" + models_output = Path(self.output_file_path()) # e.g. "demo/greeter/demo_greeter.dart" + models_stem = models_output.stem # e.g. "demo_greeter" + grpc_path = str(models_output.with_name(f"{models_stem}_grpc.dart")) lines: List[str] = [] lines.append(self.get_license_header("//")) diff --git a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py new file mode 100644 index 0000000000..047f96c636 --- /dev/null +++ b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py @@ -0,0 +1,127 @@ +# 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 analyze smoke test for emitted Dart gRPC files. + +First run on a clean machine performs `dart pub get` which downloads grpc's +transitive deps (http2, protobuf, crypto) -- about 30 seconds of network I/O. +Subsequent runs hit the local pub cache (~/.pub-cache) and complete in +under 5 seconds. + +Skipped when: + - `dart` is not on PATH, or + - the env var FORY_SKIP_DART_ANALYZE is set. + +The test does not write an `analysis_options.yaml` into tmp_path, so Dart +uses its built-in default lint set. The generated file's +`// ignore_for_file:` directive covers the lints the emitter knowingly +triggers (`non_constant_identifier_names` for `_$sayHello` / `sayHello_Pre`). +""" + +import os +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +from fory_compiler.frontend.fdl.lexer import Lexer +from fory_compiler.frontend.fdl.parser import Parser +from fory_compiler.generators.base import GeneratorOptions +from fory_compiler.generators.dart import DartGenerator + +REPO_ROOT = Path(__file__).resolve().parents[3] +assert (REPO_ROOT / "dart" / "packages" / "fory" / "pubspec.yaml").exists(), ( + f"Repo root resolution wrong: {REPO_ROOT}" +) + +_GREETER_FDL = textwrap.dedent( + """ + package demo.greeter; + + message HelloRequest { + string name = 1; + } + + message HelloReply { + string reply = 1; + } + + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + } + """ +) + + +@pytest.mark.skipif( + shutil.which("dart") is None or bool(os.environ.get("FORY_SKIP_DART_ANALYZE")), + reason="dart not on PATH or FORY_SKIP_DART_ANALYZE set", +) +def test_dart_analyze_accepts_generated_grpc_file(tmp_path: Path) -> None: + schema = Parser(Lexer(_GREETER_FDL).tokenize()).parse() + options_grpc = GeneratorOptions(output_dir=tmp_path, grpc=True) + generator = DartGenerator(schema, options_grpc) + + for file in generator.generate(): + out = tmp_path / file.path + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(file.content) + for file in generator.generate_services(): + out = tmp_path / file.path + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(file.content) + + fory_path = REPO_ROOT / "dart" / "packages" / "fory" + pubspec = textwrap.dedent( + f""" + name: fory_grpc_smoke + environment: + sdk: ^3.7.0 + dependencies: + grpc: ^4.0.0 + fory: + path: {fory_path} + """ + ).strip() + "\n" + (tmp_path / "pubspec.yaml").write_text(pubspec) + + subprocess.run( + ["dart", "pub", "get"], + cwd=tmp_path, + check=True, + ) + + # Analyze only the emitted grpc file. The companion messages file + # references a build_runner-generated `.fory.dart` part that this + # compiler does not produce (it is emitted by `package:fory`'s + # source_gen at user-build time), so analyzing the whole tree would + # surface unrelated errors. The grpc file's own correctness is what + # this smoke test exists to gate. + grpc_file = tmp_path / "demo" / "greeter" / "demo_greeter_grpc.dart" + assert grpc_file.exists(), f"grpc file not emitted: {grpc_file}" + result = subprocess.run( + ["dart", "analyze", "--fatal-warnings", str(grpc_file)], + cwd=tmp_path, + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"dart analyze failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 9a6ecb99cc..92fd4c14e3 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -260,8 +260,8 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): schema = parse_fdl(_GREETER_WITH_SERVICE) files = generate_service_files(schema, DartGenerator) - assert set(files) == {"demo_greeter_grpc.dart"} - content = files["demo_greeter_grpc.dart"] + 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 @@ -379,12 +379,12 @@ def test_dart_grpc_proto_and_fbs_service_codegen(): ) ) proto_dart = generate_service_files(proto_schema, DartGenerator) - assert "demo_proto_grpc.dart" in proto_dart + assert "demo/proto/demo_proto_grpc.dart" in proto_dart assert ( "'/demo.proto.ProtoSvc/Call'," - in proto_dart["demo_proto_grpc.dart"] + in proto_dart["demo/proto/demo_proto_grpc.dart"] ) - assert "class ProtoSvcClient extends Client {" in proto_dart["demo_proto_grpc.dart"] + assert "class ProtoSvcClient extends Client {" in proto_dart["demo/proto/demo_proto_grpc.dart"] fbs_schema = parse_fbs( dedent( @@ -401,9 +401,9 @@ def test_dart_grpc_proto_and_fbs_service_codegen(): ) ) fbs_dart = generate_service_files(fbs_schema, DartGenerator) - assert "demo_fbs_grpc.dart" in fbs_dart - assert "'/demo.fbs.FbsSvc/Call'," in fbs_dart["demo_fbs_grpc.dart"] - assert "class FbsSvcClient extends Client {" in fbs_dart["demo_fbs_grpc.dart"] + 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_java_outer_classname_service_references_nested_model_types(): From 34fd5642fb21f27d03e0b28d95106882415376aa Mon Sep 17 00:00:00 2001 From: yash Date: Sun, 31 May 2026 18:58:28 +0530 Subject: [PATCH 10/18] fix(compiler): emit dart-format-clean dart grpc output dart format reflowed the ClientMethod constant and $addMethod call; align emitter strings with format output and add a dart format gate to the smoke test. --- .../fory_compiler/generators/services/dart.py | 34 +++++++++---------- .../tests/test_dart_grpc_analyze.py | 12 +++++++ .../tests/test_service_codegen.py | 9 +++-- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index ad91f53354..0bba6ad6c6 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -131,14 +131,12 @@ def generate_dart_grpc_client(self, service: Service) -> List[str]: req_t = f"_models.{method.request_type.name}" res_t = f"_models.{method.response_type.name}" full_path = self.get_grpc_method_path(service, method) - lines.append( - f" static final {method_const} = " - f"ClientMethod<{req_t}, {res_t}>(" - ) - lines.append(f" '{full_path}',") - lines.append(" _serialize,") - lines.append(" _deserialize,") - lines.append(" );") + 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, " @@ -174,18 +172,18 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: req_t = f"_models.{method.request_type.name}" res_t = f"_models.{method.response_type.name}" 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(" false,") + lines.append(" false,") lines.append( - f" $addMethod(ServiceMethod<{req_t}, {res_t}>(" - ) - lines.append(f" '{method.name}',") - lines.append(f" {method_name}_Pre,") - lines.append(" false,") - lines.append(" false,") - lines.append( - f" (List value) => _deserialize<{req_t}>(value)," + f" (List value) => _deserialize<{req_t}>(value)," ) - lines.append(f" ({res_t} value) => _serialize(value),") - lines.append(" ));") + lines.append(f" ({res_t} value) => _serialize(value),") + lines.append(" ),") + lines.append(" );") lines.append(" }") lines.append("") for idx, method in enumerate(service.methods): diff --git a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py index 047f96c636..e3bb835f68 100644 --- a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py +++ b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py @@ -115,6 +115,18 @@ def test_dart_analyze_accepts_generated_grpc_file(tmp_path: Path) -> None: # this smoke test exists to gate. grpc_file = tmp_path / "demo" / "greeter" / "demo_greeter_grpc.dart" assert grpc_file.exists(), f"grpc file not emitted: {grpc_file}" + + fmt = subprocess.run( + ["dart", "format", "--set-exit-if-changed", str(grpc_file)], + cwd=tmp_path, + check=False, + capture_output=True, + text=True, + ) + assert fmt.returncode == 0, ( + f"dart format flagged changes:\nstdout:\n{fmt.stdout}\nstderr:\n{fmt.stderr}" + ) + result = subprocess.run( ["dart", "analyze", "--fatal-warnings", str(grpc_file)], cwd=tmp_path, diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 92fd4c14e3..d3fdd6b911 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -275,10 +275,8 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): assert "is Uint8List ? bytes : Uint8List.fromList(bytes)" in content assert "class GreeterClient extends Client {" in content - assert ( - "static final _$sayHello = " - "ClientMethod<_models.HelloRequest, _models.HelloReply>(" - ) 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 @@ -288,8 +286,9 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): 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 ( - "$addMethod(ServiceMethod<_models.HelloRequest, _models.HelloReply>(" + "ServiceMethod<_models.HelloRequest, _models.HelloReply>(" ) in content assert "'SayHello'," in content assert "sayHello_Pre," in content From 2f7d69eb7a8e7be099e154581407e64b32f4819a Mon Sep 17 00:00:00 2001 From: yash Date: Tue, 2 Jun 2026 08:06:45 +0530 Subject: [PATCH 11/18] style(compiler): ruff format dart grpc files --- .../fory_compiler/generators/services/dart.py | 12 +++++------- .../tests/test_dart_grpc_analyze.py | 9 ++++++--- .../tests/test_service_codegen.py | 18 +++++++++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index 0bba6ad6c6..63312a493d 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -80,7 +80,9 @@ def check_dart_grpc_method_collisions(self, services: List[Service]) -> None: 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/demo_greeter.dart" + models_output = Path( + self.output_file_path() + ) # e.g. "demo/greeter/demo_greeter.dart" models_stem = models_output.stem # e.g. "demo_greeter" grpc_path = str(models_output.with_name(f"{models_stem}_grpc.dart")) @@ -163,9 +165,7 @@ 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(f" String get $name => '{self.get_grpc_service_name(service)}';") lines.append("") lines.append(f" {service.name}ServiceBase() {{") for method in service.methods: @@ -178,9 +178,7 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: lines.append(f" {method_name}_Pre,") lines.append(" false,") lines.append(" false,") - lines.append( - f" (List value) => _deserialize<{req_t}>(value)," - ) + lines.append(f" (List value) => _deserialize<{req_t}>(value),") lines.append(f" ({res_t} value) => _serialize(value),") lines.append(" ),") lines.append(" );") diff --git a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py index e3bb835f68..9d019f4290 100644 --- a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py +++ b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py @@ -88,8 +88,9 @@ def test_dart_analyze_accepts_generated_grpc_file(tmp_path: Path) -> None: out.write_text(file.content) fory_path = REPO_ROOT / "dart" / "packages" / "fory" - pubspec = textwrap.dedent( - f""" + pubspec = ( + textwrap.dedent( + f""" name: fory_grpc_smoke environment: sdk: ^3.7.0 @@ -98,7 +99,9 @@ def test_dart_analyze_accepts_generated_grpc_file(tmp_path: Path) -> None: fory: path: {fory_path} """ - ).strip() + "\n" + ).strip() + + "\n" + ) (tmp_path / "pubspec.yaml").write_text(pubspec) subprocess.run( diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index d3fdd6b911..3130db5cf4 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -278,7 +278,9 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): 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 ( + "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 @@ -287,9 +289,7 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): 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 ("ServiceMethod<_models.HelloRequest, _models.HelloReply>(") in content assert "'SayHello'," in content assert "sayHello_Pre," in content assert "Future<_models.HelloReply> sayHello_Pre(" in content @@ -380,10 +380,12 @@ def test_dart_grpc_proto_and_fbs_service_codegen(): proto_dart = generate_service_files(proto_schema, DartGenerator) assert "demo/proto/demo_proto_grpc.dart" in proto_dart assert ( - "'/demo.proto.ProtoSvc/Call'," + "'/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"] ) - assert "class ProtoSvcClient extends Client {" in proto_dart["demo/proto/demo_proto_grpc.dart"] fbs_schema = parse_fbs( dedent( @@ -402,7 +404,9 @@ def test_dart_grpc_proto_and_fbs_service_codegen(): 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"] + assert ( + "class FbsSvcClient extends Client {" in fbs_dart["demo/fbs/demo_fbs_grpc.dart"] + ) def test_java_outer_classname_service_references_nested_model_types(): From 755bd6e1ac7ede2aa2b0b0ba9749d5181833ce99 Mon Sep 17 00:00:00 2001 From: yash Date: Mon, 15 Jun 2026 09:51:53 +0530 Subject: [PATCH 12/18] feat(compiler): emit dart grpc streaming methods Generate server-stream, client-stream, and bidi methods alongside unary, and serialize with ref tracking so payloads interop with the Java and Python peers. --- .../fory_compiler/generators/services/dart.py | 168 +++++++++++++----- .../tests/test_dart_grpc_analyze.py | 3 + .../tests/test_service_codegen.py | 95 ++++++++-- docs/compiler/generated-code.md | 38 ++++ 4 files changed, 239 insertions(+), 65 deletions(-) diff --git a/compiler/fory_compiler/generators/services/dart.py b/compiler/fory_compiler/generators/services/dart.py index b1988d5a97..8058e9ac9a 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -25,7 +25,7 @@ class DartServiceGeneratorMixin: - """Generates Dart gRPC service companions (unary RPCs only).""" + """Generates Dart gRPC service companions for all four RPC modes.""" def generate_services(self) -> List[GeneratedFile]: local_services = [ @@ -35,25 +35,10 @@ def generate_services(self) -> List[GeneratedFile]: ] if not local_services: return [] - self.check_dart_streaming_unsupported(local_services) self.check_dart_grpc_service_collisions(local_services) self.check_dart_grpc_method_collisions(local_services) return [self.generate_grpc_module(local_services)] - def check_dart_streaming_unsupported(self, services: List[Service]) -> None: - offenders = [] - for service in services: - for method in service.methods: - if method.client_streaming or method.server_streaming: - offenders.append(f"{service.name}.{method.name}") - if offenders: - joined = "\n - " + "\n - ".join(offenders) - raise ValueError( - "Dart gRPC generator does not yet support streaming RPCs;\n" - "remove `stream` from the following methods or omit --grpc for dart:" - + joined - ) - def check_dart_grpc_service_collisions(self, services: List[Service]) -> None: generated_names = set(self._top_level_names()) service_names = set() @@ -108,7 +93,7 @@ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: lines.append("") fory = f"_models.{self.module_type_name()}.getFory()" lines.append("List _serialize(T value) =>") - lines.append(f" {fory}.serialize(value);") + lines.append(f" {fory}.serialize(value, trackRef: true);") lines.append("") lines.append("T _deserialize(List bytes) {") lines.append( @@ -146,22 +131,76 @@ def generate_dart_grpc_client(self, service: Service) -> List[str]: "{super.options, super.interceptors});" ) for method in service.methods: - method_const = f"_${self.dart_grpc_method_name(method)}" - req_t = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" - method_name = self.dart_grpc_method_name(method) lines.append("") - lines.append(f" ResponseFuture<{res_t}> {method_name}(") - lines.append(f" {req_t} request, {{") - lines.append(" CallOptions? options,") - lines.append(" }) {") - lines.append( - f" return $createUnaryCall({method_const}, request, options: options);" - ) - 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 = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + 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 {{") @@ -170,6 +209,7 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: 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 = f"_models.{method.request_type.name}" res_t = f"_models.{method.response_type.name}" method_name = self.dart_grpc_method_name(method) @@ -177,8 +217,8 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: lines.append(f" ServiceMethod<{req_t}, {res_t}>(") lines.append(f" '{method.name}',") lines.append(f" {method_name}_Pre,") - lines.append(" false,") - lines.append(" false,") + 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(" ),") @@ -186,28 +226,58 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: lines.append(" }") lines.append("") for idx, method in enumerate(service.methods): - req_t = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" - method_name = self.dart_grpc_method_name(method) - lines.append( - " // protoc_plugin parity: _Pre shim awaits the request future," - ) - lines.append(" // then delegates to the user-overridable method.") - lines.append(f" Future<{res_t}> {method_name}_Pre(") - lines.append(" ServiceCall $call,") - lines.append(f" Future<{req_t}> $request,") - lines.append(" ) async {") - lines.append(f" return {method_name}($call, await $request);") - lines.append(" }") - lines.append("") - lines.append(f" Future<{res_t}> {method_name}(") - lines.append(" ServiceCall call,") - lines.append(f" {req_t} request,") - lines.append(" );") + 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 = f"_models.{method.request_type.name}" + res_t = f"_models.{method.response_type.name}" + 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_dart_grpc_analyze.py b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py index 9d019f4290..f691f5f8e7 100644 --- a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py +++ b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py @@ -64,6 +64,9 @@ service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); + rpc SayHellos (HelloRequest) returns (stream HelloReply); + rpc CollectHellos (stream HelloRequest) returns (HelloReply); + rpc ChatHellos (stream HelloRequest) returns (stream HelloReply); } """ ) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index f20c645182..e6582ed8e0 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -2868,7 +2868,7 @@ def test_rust_grpc_rejects_unsafe_refs(): generator.generate_services() -def test_dart_grpc_rejects_streaming(): +def test_dart_grpc_streaming_shapes(): from fory_compiler.generators.dart import DartGenerator schema = parse_fdl( @@ -2880,26 +2880,87 @@ def test_dart_grpc_rejects_streaming(): message Res {} service Streamer { - rpc Unary (Req) returns (Res); - rpc Server (Req) returns (stream Res); - rpc Client (stream Req) returns (Res); - rpc Bidi (stream Req) returns (stream Res); + rpc UnaryMessage (Req) returns (Res); + rpc ServerStreamMessage (Req) returns (stream Res); + rpc ClientStreamMessage (stream Req) returns (Res); + rpc BidiStreamMessage (stream Req) returns (stream Res); } """ ) ) - import pytest + content = generate_service_files(schema, DartGenerator)[ + "demo/streams/demo_streams_grpc.dart" + ] - with pytest.raises(ValueError) as excinfo: - generate_service_files(schema, DartGenerator) + assert "ResponseFuture<_models.Res> unaryMessage(" in content + assert "$createUnaryCall(_$unaryMessage, request, options: options);" in content - msg = str(excinfo.value) - assert "Dart gRPC generator does not yet support streaming RPCs" in msg - assert "Streamer.Server" in msg - assert "Streamer.Client" in msg - assert "Streamer.Bidi" in msg - assert "Streamer.Unary" not in msg + 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(): @@ -2917,7 +2978,10 @@ def test_dart_grpc_service_codegen_uses_fory_codec(): assert "import 'package:grpc/grpc.dart';" in content assert "import 'demo_greeter.dart' as _models;" in content - assert "_models.DemoGreeterForyModule.getFory().serialize(value)" 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 @@ -2978,7 +3042,6 @@ def test_dart_grpc_service_class_collision(): def test_dart_grpc_method_collision(): from fory_compiler.generators.dart import DartGenerator - # `SayHello` and `say_hello` both normalize to camelCase `sayHello`. schema = parse_fdl( dedent( """ diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md index 9180887bb8..d98b44ae55 100644 --- a/docs/compiler/generated-code.md +++ b/docs/compiler/generated-code.md @@ -1370,6 +1370,44 @@ 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` and serializes each request and response with +the schema module's `getFory()` instance (for example +`GreeterForyModule.getFory()`). + +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, single requests arrive as `Future` and +streaming requests as `Stream`; implementations override the abstract methods, +returning 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 From 4292d303a07cd6e97b718de2f6377be6bb30b5a4 Mon Sep 17 00:00:00 2001 From: yash Date: Mon, 15 Jun 2026 09:53:13 +0530 Subject: [PATCH 13/18] test(grpc): add java-dart grpc interop test Add a Dart interop peer (integration_tests/grpc_tests/dart) mirroring the Python peer's FDL transforms with client and server subcommands, driven both directions by DartGrpcInteropTest over the shared GrpcTestBase via new dartCommand/runDart helpers. Generate Dart stubs in generate_grpc.py, build the peer (dart pub get + build_runner) in run_tests.sh, and add a grpc_java_dart_tests CI job. --- .github/workflows/ci.yml | 44 +++ integration_tests/grpc_tests/dart/.gitignore | 4 + .../grpc_tests/dart/bin/interop.dart | 289 ++++++++++++++++++ .../grpc_tests/dart/pubspec.yaml | 15 + integration_tests/grpc_tests/generate_grpc.py | 2 + .../fory/grpc_tests/DartGrpcInteropTest.java | 43 +++ .../apache/fory/grpc_tests/GrpcTestBase.java | 44 +++ integration_tests/grpc_tests/run_tests.sh | 5 +- 8 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 integration_tests/grpc_tests/dart/.gitignore create mode 100644 integration_tests/grpc_tests/dart/bin/interop.dart create mode 100644 integration_tests/grpc_tests/dart/pubspec.yaml create mode 100644 integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcInteropTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dee6260097..b59767df2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -855,6 +855,50 @@ jobs: cd integration_tests/grpc_tests/java mvn -T16 --no-transfer-progress -Dtest=KotlinGrpcInteropTest 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@v1 + 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: Run Java/Dart gRPC Tests + run: | + cd integration_tests/grpc_tests/java + mvn -T16 --no-transfer-progress -Dtest=DartGrpcInteropTest test + javascript: name: JavaScript CI needs: changes 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..dc7197ec95 --- /dev/null +++ b/integration_tests/grpc_tests/dart/bin/interop.dart @@ -0,0 +1,289 @@ +// 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:fory/fory.dart'; +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'; + +void _installFory() { + GrpcFdlForyModule.install(Fory(compatible: true)); +} + +GrpcFdlRequest _request(String id, int count, String payload) { + final r = GrpcFdlRequest(); + r.id = id; + r.count = count; + r.payload = payload; + return r; +} + +GrpcFdlResponse _response(GrpcFdlRequest request, String tag, int offset) { + final r = GrpcFdlResponse(); + r.id = '$tag:${request.id}'; + r.count = request.count + offset; + r.payload = '$tag:${request.payload}'; + return r; +} + +GrpcFdlResponse _aggregate(List requests) { + final r = GrpcFdlResponse(); + r.id = 'client:${requests.map((e) => e.id).join('+')}'; + r.count = requests.fold(0, (sum, e) => sum + e.count); + r.payload = 'client:${requests.map((e) => e.payload).join('+')}'; + return r; +} + +GrpcFdlUnion _unionRequest(GrpcFdlRequest request) => + GrpcFdlUnion.request(request); + +GrpcFdlUnion _unionResponse(GrpcFdlRequest request, String tag, int offset) => + GrpcFdlUnion.response(_response(request, tag, offset)); + +GrpcFdlUnion _unionAggregate(List requests) => + GrpcFdlUnion.response(_aggregate(requests)); + +GrpcFdlRequest _requestFromUnion(GrpcFdlUnion union) => union.requestValue; + +// ---- Assertion helpers: throw on mismatch so main exits non-zero ---- + +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]'); + } +} + +class FdlService extends FdlGrpcServiceServiceBase { + @override + Future unaryMessage( + ServiceCall call, + GrpcFdlRequest request, + ) async { + return _response(request, 'unary', 10); + } + + @override + Stream serverStreamMessage( + ServiceCall call, + GrpcFdlRequest request, + ) async* { + for (var index = 0; index < 3; index++) { + yield _response(request, 'server-$index', index); + } + } + + @override + Future clientStreamMessage( + ServiceCall call, + Stream request, + ) async { + return _aggregate(await request.toList()); + } + + @override + Stream bidiStreamMessage( + ServiceCall call, + Stream request, + ) async* { + var index = 0; + await for (final value in request) { + yield _response(value, 'bidi-$index', index); + index++; + } + } + + @override + Future unaryUnion( + ServiceCall call, + GrpcFdlUnion request, + ) async { + return _unionResponse(_requestFromUnion(request), 'unary', 10); + } + + @override + Stream serverStreamUnion( + ServiceCall call, + GrpcFdlUnion request, + ) async* { + final item = _requestFromUnion(request); + for (var index = 0; index < 3; index++) { + yield _unionResponse(item, 'server-$index', index); + } + } + + @override + Future clientStreamUnion( + ServiceCall call, + Stream request, + ) async { + final requests = []; + await for (final item in request) { + requests.add(_requestFromUnion(item)); + } + return _unionAggregate(requests); + } + + @override + Stream bidiStreamUnion( + ServiceCall call, + Stream request, + ) async* { + var index = 0; + await for (final item in request) { + yield _unionResponse(_requestFromUnion(item), 'bidi-$index', index); + index++; + } + } +} + +Future _exerciseMessages(FdlGrpcServiceClient stub) async { + final requests = [ + _request('fdl-a', 1, 'alpha'), + _request('fdl-b', 2, 'beta'), + ]; + final first = requests[0]; + + _expect( + await stub.unaryMessage(first), + _response(first, 'unary', 10), + 'unaryMessage', + ); + + _expectList(await stub.serverStreamMessage(first).toList(), [ + for (var i = 0; i < 3; i++) _response(first, 'server-$i', i), + ], 'serverStreamMessage'); + + _expect( + await stub.clientStreamMessage(Stream.fromIterable(requests)), + _aggregate(requests), + 'clientStreamMessage', + ); + + _expectList( + await stub.bidiStreamMessage(Stream.fromIterable(requests)).toList(), + [ + for (var i = 0; i < requests.length; i++) + _response(requests[i], 'bidi-$i', i), + ], + 'bidiStreamMessage', + ); +} + +Future _exerciseUnions(FdlGrpcServiceClient stub) async { + final requests = [ + _request('fdl-u-a', 3, 'union-alpha'), + _request('fdl-u-b', 4, 'union-beta'), + ]; + final unions = [for (final r in requests) _unionRequest(r)]; + final first = requests[0]; + + _expect( + await stub.unaryUnion(unions[0]), + _unionResponse(first, 'unary', 10), + 'unaryUnion', + ); + + _expectList(await stub.serverStreamUnion(unions[0]).toList(), [ + for (var i = 0; i < 3; i++) _unionResponse(first, 'server-$i', i), + ], 'serverStreamUnion'); + + _expect( + await stub.clientStreamUnion(Stream.fromIterable(unions)), + _unionAggregate(requests), + 'clientStreamUnion', + ); + + _expectList( + await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(), + [ + for (var i = 0; i < requests.length; i++) + _unionResponse(requests[i], 'bidi-$i', i), + ], + 'bidiStreamUnion', + ); +} + +Future _runClient(String target) async { + final parts = target.split(':'); + final host = parts[0]; + final port = int.parse(parts[1]); + final channel = ClientChannel( + host, + port: port, + options: const ChannelOptions(credentials: ChannelCredentials.insecure()), + ); + try { + final stub = FdlGrpcServiceClient(channel); + await _exerciseMessages(stub); + await _exerciseUnions(stub); + } finally { + await channel.shutdown(); + } +} + +Future _runServer(String portFilePath) async { + final server = Server.create(services: [FdlService()]); + await server.serve(address: InternetAddress.loopbackIPv4, port: 0); + final port = server.port!; + await File(portFilePath).writeAsString('$port', flush: true); + // Block forever; the Java harness terminates this process. + 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 { + _installFory(); + 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..7da4e31329 --- /dev/null +++ b/integration_tests/grpc_tests/dart/pubspec.yaml @@ -0,0 +1,15 @@ +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 9c45e59258..ec5c318942 100644 --- a/integration_tests/grpc_tests/generate_grpc.py +++ b/integration_tests/grpc_tests/generate_grpc.py @@ -37,6 +37,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", } @@ -80,6 +81,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/DartGrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcInteropTest.java new file mode 100644 index 0000000000..78a05a659b --- /dev/null +++ b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcInteropTest.java @@ -0,0 +1,43 @@ +/* + * 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 DartGrpcInteropTest extends GrpcTestBase { + + @Test + public void testJavaServerDartClient() throws Exception { + Server server = startJavaFdlServer(); + 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::exerciseFdl); + } +} 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 8e9fe616e7..60116949a6 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 repoRoot = repoRoot(); Path grpcRoot = repoRoot.resolve("integration_tests").resolve("grpc_tests"); diff --git a/integration_tests/grpc_tests/run_tests.sh b/integration_tests/grpc_tests/run_tests.sh index c1e532e94d..6c3d739887 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:-PythonGrpcInteropTest,RustGrpcInteropTest,GoGrpcInteropTest,KotlinGrpcInteropTest}" +TEST_CLASSES="${1:-PythonGrpcInteropTest,RustGrpcInteropTest,GoGrpcInteropTest,KotlinGrpcInteropTest,DartGrpcInteropTest}" python -m pip install "grpcio>=1.62.2,<1.71" python -m pip install -v -e "${ROOT_DIR}/python" @@ -33,6 +33,9 @@ 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 cd "${ROOT_DIR}/integration_tests/grpc_tests/java" mvn -T16 --no-transfer-progress \ -Dtest="${TEST_CLASSES}" \ From e75e61f33410a8b483d6ceb8b8a1c593fcc818c2 Mon Sep 17 00:00:00 2001 From: yash Date: Mon, 15 Jun 2026 10:10:18 +0530 Subject: [PATCH 14/18] style(compiler): ruff format test service file --- compiler/fory_compiler/tests/test_service_codegen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index e6582ed8e0..2c42e2b04a 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -2919,7 +2919,7 @@ def test_dart_grpc_streaming_shapes(): assert ( "$createStreamingCall(_$bidiStreamMessage, request, options: options);" ) in content - + assert ( "'UnaryMessage',\n unaryMessage_Pre,\n false,\n false," ) in content From f6880f4d4a1980ea8d6a34f6edf9658da9c94eab Mon Sep 17 00:00:00 2001 From: yash Date: Mon, 15 Jun 2026 20:42:36 +0530 Subject: [PATCH 15/18] docs: document dart grpc support --- .agents/languages/dart.md | 2 + docs/compiler/compiler-guide.md | 14 +- docs/compiler/flatbuffers-idl.md | 11 +- docs/compiler/index.md | 15 +- docs/compiler/protobuf-idl.md | 37 ++-- docs/compiler/schema-idl.md | 6 +- docs/guide/dart/grpc-support.md | 310 +++++++++++++++++++++++++++++ docs/guide/dart/index.md | 1 + docs/guide/dart/troubleshooting.md | 25 +++ 9 files changed, 383 insertions(+), 38 deletions(-) create mode 100644 docs/guide/dart/grpc-support.md diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md index 616a4cc0d0..e9d2c4a294 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. Service companions must resolve their `Fory` through that module's `getFory()` and must not introduce package-derived aliases or duplicate serializer registration paths. ## Commands diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md index a89880bb20..df80d29c52 100644 --- a/docs/compiler/compiler-guide.md +++ b/docs/compiler/compiler-guide.md @@ -142,16 +142,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 @@ -160,7 +160,8 @@ output imports grpc-go, Rust output imports `tonic` and `bytes`, 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`. +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. @@ -432,6 +433,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 abf6652189..790af37316 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,13 +138,14 @@ 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. Fory packages do not add gRPC as a hard dependency. +`@grpc/grpc-js`, C# `Grpc.Core.Api` plus server/client dependencies, or Dart +`package:grpc` when they compile or run those files. Fory packages do not add +gRPC as a hard dependency. Use `--grpc-web` with JavaScript output to generate browser clients that import `grpc-web`. diff --git a/docs/compiler/index.md b/docs/compiler/index.md index be81f5bfc2..ea625242ff 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,19 +88,20 @@ 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. 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. 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 2c23602429..6b81623e89 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,17 +315,18 @@ 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 import `grpc`, 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-web` +generated C# service files import `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-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 diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md index 28da5fa7d8..bfa8567c47 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,8 +952,8 @@ 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. + 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`. **Grammar:** diff --git a/docs/guide/dart/grpc-support.md b/docs/guide/dart/grpc-support.md new file mode 100644 index 0000000000..792cd5ebd1 --- /dev/null +++ b/docs/guide/dart/grpc-support.md @@ -0,0 +1,310 @@ +--- +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: + +| File | Purpose | +| ------------------------------------------------ | ---------------------------------------------------- | +| `demo/greeter/demo_greeter.dart` | Fory model types and the schema module | +| `demo/greeter/demo_greeter.fory.dart` | Serializers and registration (built by build_runner) | +| `demo/greeter/demo_greeter_grpc.dart` | gRPC client, service base, and method descriptors | +| `DemoGreeterForyModule` in `demo_greeter.dart` | Fory registration module for generated types | +| `GreeterServiceBase` in `demo_greeter_grpc.dart` | Base class for server implementations | +| `GreeterClient` in `demo_greeter_grpc.dart` | Client stub for gRPC calls | + +## Register Types + +The generated gRPC companion serializes through the schema module's `Fory` +instance. Create a `Fory`, install the module once at startup, and the generated +client and service base pick it up automatically: + +```dart +import 'package:fory/fory.dart'; +import 'demo/greeter/demo_greeter.dart'; + +void setUpFory() { + DemoGreeterForyModule.install(Fory()); +} +``` + +`install` registers every generated type and stores the `Fory` instance that the +companion's `getFory()` returns. Service implementations and clients do not +perform manual serializer registration. + +## 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/demo_greeter.dart'; +import 'demo/greeter/demo_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 { + DemoGreeterForyModule.install(Fory()); + 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/demo_greeter.dart'; +import 'demo/greeter/demo_greeter_grpc.dart'; + +Future main() async { + DemoGreeterForyModule.install(Fory()); + 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 schema module names come from the source file stem. They do not come from +the package and they do not come from gRPC service names. + +| Schema input | Model file | Schema module | +| ------------------ | ------------------- | ----------------------- | +| `service.fdl` | `service.dart` | `ServiceForyModule` | +| `order-events.fdl` | `order_events.dart` | `OrderEventsForyModule` | +| `greeter.fdl` | `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. + +### `getFory()` Throws Before First Use + +Call `ForyModule.install(fory)` once during startup before issuing or +serving any RPC. The generated companion resolves its `Fory` instance through +that module. + +### 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 b80bf01e58..769cd1e60f 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..82d6d7af92 100644 --- a/docs/guide/dart/troubleshooting.md +++ b/docs/guide/dart/troubleshooting.md @@ -138,9 +138,34 @@ 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). + +## `getFory()` throws before the first gRPC call + +**Cause**: The generated gRPC companion resolves its `Fory` instance from the +schema module, which must be installed first. + +**Fix**: Call `ForyModule.install(fory)` once during startup before +issuing or serving any RPC. + +## 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) From d33b55d6c0ea42d7270039e216ca47e8fc173aad Mon Sep 17 00:00:00 2001 From: yash Date: Wed, 17 Jun 2026 23:32:53 +0530 Subject: [PATCH 16/18] feat(compiler): make dart grpc self-sufficient with payload type resolution and reserved-name guard --- compiler/fory_compiler/generators/dart.py | 6 +- .../fory_compiler/generators/services/dart.py | 86 +++++++++++++---- .../tests/test_service_codegen.py | 96 +++++++++++++++++++ ...GrpcInteropTest.java => DartGrpcTest.java} | 0 4 files changed, 169 insertions(+), 19 deletions(-) rename integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/{DartGrpcInteropTest.java => DartGrpcTest.java} (100%) diff --git a/compiler/fory_compiler/generators/dart.py b/compiler/fory_compiler/generators/dart.py index f6694420f5..91a0ef7c53 100644 --- a/compiler/fory_compiler/generators/dart.py +++ b/compiler/fory_compiler/generators/dart.py @@ -1292,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 index 8058e9ac9a..d875604d7a 100644 --- a/compiler/fory_compiler/generators/services/dart.py +++ b/compiler/fory_compiler/generators/services/dart.py @@ -18,7 +18,7 @@ """Dart gRPC service generator helpers.""" from pathlib import Path -from typing import List +from typing import Dict, List, Tuple from fory_compiler.generators.base import GeneratedFile from fory_compiler.ir.ast import RpcMethod, Service @@ -27,6 +27,10 @@ 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 @@ -37,6 +41,7 @@ def generate_services(self) -> List[GeneratedFile]: 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: @@ -63,14 +68,38 @@ def check_dart_grpc_method_collisions(self, services: List[Service]) -> None: ) 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/demo_greeter.dart" - models_stem = models_output.stem # e.g. "demo_greeter" + ) # 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("") @@ -85,6 +114,8 @@ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: 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; " @@ -102,22 +133,43 @@ def generate_grpc_module(self, services: List[Service]) -> GeneratedFile: lines.append(f" return {fory}.deserialize(u8);") lines.append("}") lines.append("") - - for service in services: - lines.extend(self.generate_dart_grpc_client(service)) - lines.append("") - lines.extend(self.generate_dart_grpc_service_base(service)) - 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 = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" + 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}>(") @@ -139,8 +191,8 @@ def generate_dart_grpc_client(self, service: Service) -> List[str]: 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 = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" + 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 = ( @@ -210,8 +262,8 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: lines.append(f" {service.name}ServiceBase() {{") for method in service.methods: streaming_request, streaming_response = self._dart_grpc_call_kind(method) - req_t = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" + 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}>(") @@ -234,8 +286,8 @@ def generate_dart_grpc_service_base(self, service: Service) -> List[str]: def _dart_grpc_service_method(self, method: RpcMethod) -> List[str]: streaming_request, streaming_response = self._dart_grpc_call_kind(method) - req_t = f"_models.{method.request_type.name}" - res_t = f"_models.{method.response_type.name}" + 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 diff --git a/compiler/fory_compiler/tests/test_service_codegen.py b/compiler/fory_compiler/tests/test_service_codegen.py index 5e8b408075..9f47280343 100644 --- a/compiler/fory_compiler/tests/test_service_codegen.py +++ b/compiler/fory_compiler/tests/test_service_codegen.py @@ -3222,3 +3222,99 @@ def test_dart_grpc_proto_and_fbs_service_codegen(): 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/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcInteropTest.java b/integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java similarity index 100% rename from integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcInteropTest.java rename to integration_tests/grpc_tests/java/src/test/java/org/apache/fory/grpc_tests/DartGrpcTest.java From 0025c4a1b058c3068ced3f7c61397155852ee995 Mon Sep 17 00:00:00 2001 From: yash Date: Wed, 17 Jun 2026 23:32:53 +0530 Subject: [PATCH 17/18] docs: align dart grpc docs and agent guidance with the auto-ready Fory runtime --- .agents/languages/dart.md | 2 +- docs/compiler/generated-code.md | 15 +++--- docs/guide/dart/grpc-support.md | 77 +++++++++++------------------- docs/guide/dart/troubleshooting.md | 8 ---- 4 files changed, 39 insertions(+), 63 deletions(-) diff --git a/.agents/languages/dart.md b/.agents/languages/dart.md index e9d2c4a294..a68af06934 100644 --- a/.agents/languages/dart.md +++ b/.agents/languages/dart.md @@ -23,7 +23,7 @@ Load this file when changing `dart/`. - 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. Service companions must resolve their `Fory` through that module's `getFory()` and must not introduce package-derived aliases or duplicate serializer registration paths. +- 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/docs/compiler/generated-code.md b/docs/compiler/generated-code.md index fcdfdab29d..b3e5ebc91f 100644 --- a/docs/compiler/generated-code.md +++ b/docs/compiler/generated-code.md @@ -1384,9 +1384,11 @@ void main() { 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` and serializes each request and response with -the schema module's `getFory()` instance (for example -`GreeterForyModule.getFory()`). +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 @@ -1411,9 +1413,10 @@ abstract class GreeterServiceBase extends Service { A single-response client method returns `ResponseFuture` (client-streaming adapts the streaming call with `.single`); a streaming-response method returns -`ResponseStream`. On the server, single requests arrive as `Future` and -streaming requests as `Stream`; implementations override the abstract methods, -returning a `Future` for single responses or a `Stream` for streaming responses. +`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. diff --git a/docs/guide/dart/grpc-support.md b/docs/guide/dart/grpc-support.md index 792cd5ebd1..bc8fac968b 100644 --- a/docs/guide/dart/grpc-support.md +++ b/docs/guide/dart/grpc-support.md @@ -81,35 +81,23 @@ generated models (this step is required before the code can run): dart run build_runner build --delete-conflicting-outputs ``` -For this schema, the Dart generator emits: - -| File | Purpose | -| ------------------------------------------------ | ---------------------------------------------------- | -| `demo/greeter/demo_greeter.dart` | Fory model types and the schema module | -| `demo/greeter/demo_greeter.fory.dart` | Serializers and registration (built by build_runner) | -| `demo/greeter/demo_greeter_grpc.dart` | gRPC client, service base, and method descriptors | -| `DemoGreeterForyModule` in `demo_greeter.dart` | Fory registration module for generated types | -| `GreeterServiceBase` in `demo_greeter_grpc.dart` | Base class for server implementations | -| `GreeterClient` in `demo_greeter_grpc.dart` | Client stub for gRPC calls | - -## Register Types - -The generated gRPC companion serializes through the schema module's `Fory` -instance. Create a `Fory`, install the module once at startup, and the generated -client and service base pick it up automatically: - -```dart -import 'package:fory/fory.dart'; -import 'demo/greeter/demo_greeter.dart'; - -void setUpFory() { - DemoGreeterForyModule.install(Fory()); -} -``` - -`install` registers every generated type and stores the `Fory` instance that the -companion's `getFory()` returns. Service implementations and clients do not -perform manual serializer registration. +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 @@ -119,8 +107,8 @@ Extend the generated `GreeterServiceBase` and host it with grpc-dart's `Server`: import 'dart:io'; import 'package:grpc/grpc.dart'; -import 'demo/greeter/demo_greeter.dart'; -import 'demo/greeter/demo_greeter_grpc.dart'; +import 'demo/greeter/greeter.dart'; +import 'demo/greeter/greeter_grpc.dart'; class GreeterService extends GreeterServiceBase { @override @@ -131,7 +119,6 @@ class GreeterService extends GreeterServiceBase { } Future main() async { - DemoGreeterForyModule.install(Fory()); final server = Server.create(services: [GreeterService()]); await server.serve(address: InternetAddress.loopbackIPv4, port: 50051); } @@ -143,11 +130,10 @@ Use the generated client with a `ClientChannel`: ```dart import 'package:grpc/grpc.dart'; -import 'demo/greeter/demo_greeter.dart'; -import 'demo/greeter/demo_greeter_grpc.dart'; +import 'demo/greeter/greeter.dart'; +import 'demo/greeter/greeter_grpc.dart'; Future main() async { - DemoGreeterForyModule.install(Fory()); final channel = ClientChannel( 'localhost', port: 50051, @@ -258,14 +244,15 @@ the gRPC path, while the Dart methods use camelCase names. ## Generated Module Names -Dart schema module names come from the source file stem. They do not come from -the package and they do not come from gRPC service 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 | Model file | Schema module | -| ------------------ | ------------------- | ----------------------- | -| `service.fdl` | `service.dart` | `ServiceForyModule` | -| `order-events.fdl` | `order_events.dart` | `OrderEventsForyModule` | -| `greeter.fdl` | `greeter.dart` | `GreeterForyModule` | +| 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 @@ -291,12 +278,6 @@ All normal gRPC operational features still belong to your gRPC stack: Add `grpc` to your application dependencies. Generated Fory service files import grpc-dart APIs, but `fory` intentionally does not depend on gRPC. -### `getFory()` Throws Before First Use - -Call `ForyModule.install(fory)` once during startup before issuing or -serving any RPC. The generated companion resolves its `Fory` instance through -that module. - ### Generated Code References a Missing `.fory.dart` Part Run `dart run build_runner build --delete-conflicting-outputs` after generating diff --git a/docs/guide/dart/troubleshooting.md b/docs/guide/dart/troubleshooting.md index 82d6d7af92..5f066920a2 100644 --- a/docs/guide/dart/troubleshooting.md +++ b/docs/guide/dart/troubleshooting.md @@ -146,14 +146,6 @@ 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). -## `getFory()` throws before the first gRPC call - -**Cause**: The generated gRPC companion resolves its `Fory` instance from the -schema module, which must be installed first. - -**Fix**: Call `ForyModule.install(fory)` once during startup before -issuing or serving any RPC. - ## A protobuf client cannot decode a Fory gRPC service **Cause**: Fory gRPC companions use gRPC transports with Fory-encoded message From b60202155bc1580e1edf4fe774786be1b0e338e6 Mon Sep 17 00:00:00 2001 From: yash Date: Wed, 17 Jun 2026 23:34:07 +0530 Subject: [PATCH 18/18] test(grpc): exercise all schemas in dart interop, rename to DartGrpcTest, pin CI dart action, move analyze coverage to integration --- .github/workflows/ci.yml | 9 +- .../tests/test_dart_grpc_analyze.py | 145 ----- .../grpc_tests/dart/bin/interop.dart | 503 +++++++++++++----- .../grpc_tests/dart/pubspec.yaml | 17 + .../apache/fory/grpc_tests/DartGrpcTest.java | 7 +- integration_tests/grpc_tests/run_tests.sh | 2 + 6 files changed, 397 insertions(+), 286 deletions(-) delete mode 100644 compiler/fory_compiler/tests/test_dart_grpc_analyze.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97792f9899..065946cef0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -873,7 +873,7 @@ jobs: python-version: 3.11 cache: "pip" - name: Set up Dart - uses: dart-lang/setup-dart@v1 + uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c # v1.7.1 with: sdk: stable - name: Cache Maven local repository @@ -894,10 +894,15 @@ jobs: 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=DartGrpcInteropTest test + mvn -T16 --no-transfer-progress -Dtest=DartGrpcTest test javascript: name: JavaScript CI diff --git a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py b/compiler/fory_compiler/tests/test_dart_grpc_analyze.py deleted file mode 100644 index f691f5f8e7..0000000000 --- a/compiler/fory_compiler/tests/test_dart_grpc_analyze.py +++ /dev/null @@ -1,145 +0,0 @@ -# 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 analyze smoke test for emitted Dart gRPC files. - -First run on a clean machine performs `dart pub get` which downloads grpc's -transitive deps (http2, protobuf, crypto) -- about 30 seconds of network I/O. -Subsequent runs hit the local pub cache (~/.pub-cache) and complete in -under 5 seconds. - -Skipped when: - - `dart` is not on PATH, or - - the env var FORY_SKIP_DART_ANALYZE is set. - -The test does not write an `analysis_options.yaml` into tmp_path, so Dart -uses its built-in default lint set. The generated file's -`// ignore_for_file:` directive covers the lints the emitter knowingly -triggers (`non_constant_identifier_names` for `_$sayHello` / `sayHello_Pre`). -""" - -import os -import shutil -import subprocess -import textwrap -from pathlib import Path - -import pytest - -from fory_compiler.frontend.fdl.lexer import Lexer -from fory_compiler.frontend.fdl.parser import Parser -from fory_compiler.generators.base import GeneratorOptions -from fory_compiler.generators.dart import DartGenerator - -REPO_ROOT = Path(__file__).resolve().parents[3] -assert (REPO_ROOT / "dart" / "packages" / "fory" / "pubspec.yaml").exists(), ( - f"Repo root resolution wrong: {REPO_ROOT}" -) - -_GREETER_FDL = textwrap.dedent( - """ - package demo.greeter; - - message HelloRequest { - string name = 1; - } - - message HelloReply { - string reply = 1; - } - - service Greeter { - rpc SayHello (HelloRequest) returns (HelloReply); - rpc SayHellos (HelloRequest) returns (stream HelloReply); - rpc CollectHellos (stream HelloRequest) returns (HelloReply); - rpc ChatHellos (stream HelloRequest) returns (stream HelloReply); - } - """ -) - - -@pytest.mark.skipif( - shutil.which("dart") is None or bool(os.environ.get("FORY_SKIP_DART_ANALYZE")), - reason="dart not on PATH or FORY_SKIP_DART_ANALYZE set", -) -def test_dart_analyze_accepts_generated_grpc_file(tmp_path: Path) -> None: - schema = Parser(Lexer(_GREETER_FDL).tokenize()).parse() - options_grpc = GeneratorOptions(output_dir=tmp_path, grpc=True) - generator = DartGenerator(schema, options_grpc) - - for file in generator.generate(): - out = tmp_path / file.path - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text(file.content) - for file in generator.generate_services(): - out = tmp_path / file.path - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text(file.content) - - fory_path = REPO_ROOT / "dart" / "packages" / "fory" - pubspec = ( - textwrap.dedent( - f""" - name: fory_grpc_smoke - environment: - sdk: ^3.7.0 - dependencies: - grpc: ^4.0.0 - fory: - path: {fory_path} - """ - ).strip() - + "\n" - ) - (tmp_path / "pubspec.yaml").write_text(pubspec) - - subprocess.run( - ["dart", "pub", "get"], - cwd=tmp_path, - check=True, - ) - - # Analyze only the emitted grpc file. The companion messages file - # references a build_runner-generated `.fory.dart` part that this - # compiler does not produce (it is emitted by `package:fory`'s - # source_gen at user-build time), so analyzing the whole tree would - # surface unrelated errors. The grpc file's own correctness is what - # this smoke test exists to gate. - grpc_file = tmp_path / "demo" / "greeter" / "demo_greeter_grpc.dart" - assert grpc_file.exists(), f"grpc file not emitted: {grpc_file}" - - fmt = subprocess.run( - ["dart", "format", "--set-exit-if-changed", str(grpc_file)], - cwd=tmp_path, - check=False, - capture_output=True, - text=True, - ) - assert fmt.returncode == 0, ( - f"dart format flagged changes:\nstdout:\n{fmt.stdout}\nstderr:\n{fmt.stderr}" - ) - - result = subprocess.run( - ["dart", "analyze", "--fatal-warnings", str(grpc_file)], - cwd=tmp_path, - check=False, - capture_output=True, - text=True, - ) - assert result.returncode == 0, ( - f"dart analyze failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" - ) diff --git a/integration_tests/grpc_tests/dart/bin/interop.dart b/integration_tests/grpc_tests/dart/bin/interop.dart index dc7197ec95..21bd4736c5 100644 --- a/integration_tests/grpc_tests/dart/bin/interop.dart +++ b/integration_tests/grpc_tests/dart/bin/interop.dart @@ -18,52 +18,14 @@ import 'dart:async'; import 'dart:io'; -import 'package:fory/fory.dart'; 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'; - -void _installFory() { - GrpcFdlForyModule.install(Fory(compatible: true)); -} - -GrpcFdlRequest _request(String id, int count, String payload) { - final r = GrpcFdlRequest(); - r.id = id; - r.count = count; - r.payload = payload; - return r; -} - -GrpcFdlResponse _response(GrpcFdlRequest request, String tag, int offset) { - final r = GrpcFdlResponse(); - r.id = '$tag:${request.id}'; - r.count = request.count + offset; - r.payload = '$tag:${request.payload}'; - return r; -} - -GrpcFdlResponse _aggregate(List requests) { - final r = GrpcFdlResponse(); - r.id = 'client:${requests.map((e) => e.id).join('+')}'; - r.count = requests.fold(0, (sum, e) => sum + e.count); - r.payload = 'client:${requests.map((e) => e.payload).join('+')}'; - return r; -} - -GrpcFdlUnion _unionRequest(GrpcFdlRequest request) => - GrpcFdlUnion.request(request); - -GrpcFdlUnion _unionResponse(GrpcFdlRequest request, String tag, int offset) => - GrpcFdlUnion.response(_response(request, tag, offset)); - -GrpcFdlUnion _unionAggregate(List requests) => - GrpcFdlUnion.response(_aggregate(requests)); - -GrpcFdlRequest _requestFromUnion(GrpcFdlUnion union) => union.requestValue; - -// ---- Assertion helpers: throw on mismatch so main exits non-zero ---- +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) { @@ -84,180 +46,450 @@ void _expectList(List actual, List expected, String what) { } } +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 call, - GrpcFdlRequest request, - ) async { - return _response(request, 'unary', 10); - } + Future unaryMessage(ServiceCall c, GrpcFdlRequest r) async => + _fdlResponse(r, 'unary', 10); @override Stream serverStreamMessage( - ServiceCall call, - GrpcFdlRequest request, + ServiceCall c, + GrpcFdlRequest r, ) async* { - for (var index = 0; index < 3; index++) { - yield _response(request, 'server-$index', index); + for (var i = 0; i < 3; i++) { + yield _fdlResponse(r, 'server-$i', i); } } @override Future clientStreamMessage( - ServiceCall call, - Stream request, - ) async { - return _aggregate(await request.toList()); - } + ServiceCall c, + Stream r, + ) async => _fdlAggregate(await r.toList()); @override Stream bidiStreamMessage( - ServiceCall call, - Stream request, + ServiceCall c, + Stream r, ) async* { - var index = 0; - await for (final value in request) { - yield _response(value, 'bidi-$index', index); - index++; + var i = 0; + await for (final v in r) { + yield _fdlResponse(v, 'bidi-$i', i); + i++; } } @override - Future unaryUnion( - ServiceCall call, - GrpcFdlUnion request, - ) async { - return _unionResponse(_requestFromUnion(request), 'unary', 10); - } + Future unaryUnion(ServiceCall c, GrpcFdlUnion r) async => + _fdlUnionResponse(_fdlRequestFromUnion(r), 'unary', 10); @override - Stream serverStreamUnion( - ServiceCall call, - GrpcFdlUnion request, - ) async* { - final item = _requestFromUnion(request); - for (var index = 0; index < 3; index++) { - yield _unionResponse(item, 'server-$index', index); + 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 call, - Stream request, + ServiceCall c, + Stream r, ) async { final requests = []; - await for (final item in request) { - requests.add(_requestFromUnion(item)); + await for (final item in r) { + requests.add(_fdlRequestFromUnion(item)); } - return _unionAggregate(requests); + return _fdlUnionAggregate(requests); } @override Stream bidiStreamUnion( - ServiceCall call, - Stream request, + ServiceCall c, + Stream r, ) async* { - var index = 0; - await for (final item in request) { - yield _unionResponse(_requestFromUnion(item), 'bidi-$index', index); - index++; + var i = 0; + await for (final item in r) { + yield _fdlUnionResponse(_fdlRequestFromUnion(item), 'bidi-$i', i); + i++; } } } -Future _exerciseMessages(FdlGrpcServiceClient stub) async { - final requests = [ - _request('fdl-a', 1, 'alpha'), - _request('fdl-b', 2, 'beta'), - ]; - final first = requests[0]; +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), - _response(first, 'unary', 10), - 'unaryMessage', + _fdlResponse(first, 'unary', 10), + 'fdl.unaryMessage', ); - _expectList(await stub.serverStreamMessage(first).toList(), [ - for (var i = 0; i < 3; i++) _response(first, 'server-$i', i), - ], 'serverStreamMessage'); - + for (var i = 0; i < 3; i++) _fdlResponse(first, 'server-$i', i), + ], 'fdl.serverStreamMessage'); _expect( - await stub.clientStreamMessage(Stream.fromIterable(requests)), - _aggregate(requests), - 'clientStreamMessage', + 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.bidiStreamMessage(Stream.fromIterable(requests)).toList(), + await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(), [ - for (var i = 0; i < requests.length; i++) - _response(requests[i], 'bidi-$i', i), + for (var i = 0; i < unionReqs.length; i++) + _fdlUnionResponse(unionReqs[i], 'bidi-$i', i), ], - 'bidiStreamMessage', + 'fdl.bidiStreamUnion', ); } -Future _exerciseUnions(FdlGrpcServiceClient stub) async { - final requests = [ - _request('fdl-u-a', 3, 'union-alpha'), - _request('fdl-u-b', 4, 'union-beta'), +Future _exerciseFbs(FbsGrpcServiceClient stub) async { + final messages = [ + _fbsRequest('fbs-a', 5, 'alpha'), + _fbsRequest('fbs-b', 6, 'beta'), ]; - final unions = [for (final r in requests) _unionRequest(r)]; - final first = requests[0]; + 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]), - _unionResponse(first, 'unary', 10), - 'unaryUnion', + _fbsUnionResponse(unionFirst, 'unary', 10), + 'fbs.unaryUnion', ); - _expectList(await stub.serverStreamUnion(unions[0]).toList(), [ - for (var i = 0; i < 3; i++) _unionResponse(first, 'server-$i', i), - ], 'serverStreamUnion'); - + for (var i = 0; i < 3; i++) _fbsUnionResponse(unionFirst, 'server-$i', i), + ], 'fbs.serverStreamUnion'); _expect( await stub.clientStreamUnion(Stream.fromIterable(unions)), - _unionAggregate(requests), - 'clientStreamUnion', + _fbsUnionAggregate(unionReqs), + 'fbs.clientStreamUnion', ); - _expectList( await stub.bidiStreamUnion(Stream.fromIterable(unions)).toList(), [ - for (var i = 0; i < requests.length; i++) - _unionResponse(requests[i], 'bidi-$i', i), + for (var i = 0; i < unionReqs.length; i++) + _fbsUnionResponse(unionReqs[i], 'bidi-$i', i), ], - 'bidiStreamUnion', + '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 host = parts[0]; - final port = int.parse(parts[1]); final channel = ClientChannel( - host, - port: port, + parts[0], + port: int.parse(parts[1]), options: const ChannelOptions(credentials: ChannelCredentials.insecure()), ); try { - final stub = FdlGrpcServiceClient(channel); - await _exerciseMessages(stub); - await _exerciseUnions(stub); + 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()]); + final server = Server.create( + services: [FdlService(), FbsService(), PbService()], + ); await server.serve(address: InternetAddress.loopbackIPv4, port: 0); - final port = server.port!; - await File(portFilePath).writeAsString('$port', flush: true); - // Block forever; the Java harness terminates this process. + await File(portFilePath).writeAsString('${server.port!}', flush: true); await Completer().future; } @@ -270,7 +502,6 @@ String _flag(List args, String name) { } Future main(List args) async { - _installFory(); try { if (args.isNotEmpty && args[0] == 'client') { await _runClient(_flag(args, '--target')); diff --git a/integration_tests/grpc_tests/dart/pubspec.yaml b/integration_tests/grpc_tests/dart/pubspec.yaml index 7da4e31329..14e21dbf75 100644 --- a/integration_tests/grpc_tests/dart/pubspec.yaml +++ b/integration_tests/grpc_tests/dart/pubspec.yaml @@ -1,3 +1,20 @@ +# 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 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 index 78a05a659b..4d9ce055a0 100644 --- 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 @@ -23,11 +23,11 @@ import java.util.concurrent.TimeUnit; import org.testng.annotations.Test; -public class DartGrpcInteropTest extends GrpcTestBase { +public class DartGrpcTest extends GrpcTestBase { @Test public void testJavaServerDartClient() throws Exception { - Server server = startJavaFdlServer(); + Server server = startJavaAllSchemasServer(); try { runDart("dart-grpc-client", "client", "--target", "127.0.0.1:" + server.getPort()); } finally { @@ -38,6 +38,7 @@ public void testJavaServerDartClient() throws Exception { @Test public void testDartServerJavaClient() throws Exception { - exercisePeerServer("dart-grpc", "Dart", "fory-grpc-dart-", dartCommand("server"), this::exerciseFdl); + exercisePeerServer( + "dart-grpc", "Dart", "fory-grpc-dart-", dartCommand("server"), this::exerciseAllSchemas); } } diff --git a/integration_tests/grpc_tests/run_tests.sh b/integration_tests/grpc_tests/run_tests.sh index 9d7527480c..9057727d9d 100755 --- a/integration_tests/grpc_tests/run_tests.sh +++ b/integration_tests/grpc_tests/run_tests.sh @@ -36,6 +36,8 @@ 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}" \