chaokunyang commented on code in PR #3698: URL: https://github.com/apache/fory/pull/3698#discussion_r3401341441
########## compiler/fory_compiler/generators/services/go.py: ########## @@ -0,0 +1,720 @@ +# 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. + +"""Go gRPC service code generator.""" + +from typing import List +from fory_compiler.generators.services.base import ( + ImportTracker, + StreamingMode, + streaming_mode, +) +from fory_compiler.generators.base import GeneratedFile +from fory_compiler.ir.ast import Service, NamedType + + +class GoServiceGeneratorMixin: + """Generates Go gRPC service stubs.""" + + def generate_services(self) -> List[GeneratedFile]: + local_services = [ + s for s in self.schema.services if not self.is_imported_type(s) + ] + if not local_services: + return [] + return [self._generate_grpc_file(local_services)] + + def _generate_grpc_file(self, services: List[Service]) -> GeneratedFile: + """Generate one _grpc.go file containing all services in the schema.""" + lines: List[str] = [] + tracker = ImportTracker() + + # License header + lines.append(self.get_license_header("//")) + lines.append("") + + # Package declaration + lines.append(f"package {self.get_package_name()}") + lines.append("") + + # Save placeholder index so imports can be inserted after all code is + # generated (imports are collected lazily via tracker as types are resolved). + import_placeholder_index = len(lines) + + # CodecV2 is emitted once per file, shared by all services in the schema. + lines.extend(self._generate_codec()) + + for service in services: + lines.extend(self._generate_client_interface(service, tracker)) + lines.extend(self._generate_client_struct(service)) + lines.extend(self._generate_new_client(service)) + lines.extend(self._generate_client_methods(service, tracker)) + lines.extend(self._generate_stream_types(service, tracker)) + lines.extend(self._generate_server_interface(service, tracker)) + lines.extend(self._generate_unimplemented_server(service, tracker)) + lines.extend(self._generate_server_stream_types(service, tracker)) + lines.extend(self._generate_service_desc(service, tracker)) + lines.extend(self._generate_register_server(service)) + + import_lines = self._build_import_block(tracker) + for i, line in enumerate(import_lines): + lines.insert(import_placeholder_index + i, line) + + return GeneratedFile( + path=f"{self.get_file_name()}_grpc.go", content="\n".join(lines) + ) + + def _build_import_block(self, tracker: ImportTracker) -> List[str]: + imports = [ + '"context"', + '"google.golang.org/grpc"', + '"google.golang.org/grpc/codes"', + '"google.golang.org/grpc/mem"', + '"google.golang.org/grpc/status"', + '"github.com/apache/fory/go/fory"', + ] + + for alias, path in tracker._imports.items(): + imports.append(f'{alias} "{path}"') + + sorted_imports = sorted(set(imports)) + + lines = ["import ("] + for imp in sorted_imports: + lines.append(f"\t{imp}") + lines.append(")") + lines.append("") + + return lines + + def _resolve_go_type(self, named_type: NamedType, tracker: ImportTracker) -> str: + type_ref = self.schema.resolve_type_name(named_type.name) + type_def = self.schema.get_type(type_ref) + if type_def is not None and self.is_imported_type(type_def): + info = self._import_info_for_type(type_def) + if info: + alias, import_path, _ = info + tracker.add(alias, import_path) + return f"*{alias}.{type_ref}" + return f"*{type_ref}" + + def _generate_client_interface( + self, service: Service, tracker: ImportTracker + ) -> List[str]: + lines: List[str] = [] + lines.append( + f"// {service.name}Client is the client API for {service.name} service." + ) + lines.append(f"type {service.name}Client interface {{") + for method in service.methods: + req_type = self._resolve_go_type(method.request_type, tracker) + res_type = self._resolve_go_type(method.response_type, tracker) + mode = streaming_mode(method) + if mode is StreamingMode.UNARY: + signature = f"(ctx context.Context, in {req_type}, opts ...grpc.CallOption) ({res_type}, error)" + lines.append(f"\t{self.to_pascal_case(method.name)}{signature}") + elif mode is StreamingMode.SERVER_STREAMING: + signature = f"(ctx context.Context, in {req_type}, opts ...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, error)" + lines.append(f"\t{self.to_pascal_case(method.name)}{signature}") + else: + signature = f"(ctx context.Context, opts ...grpc.CallOption) ({service.name}_{self.to_pascal_case(method.name)}Client, error)" + lines.append(f"\t{self.to_pascal_case(method.name)}{signature}") + + lines.append("}") + lines.append("") + return lines + + def _generate_client_struct(self, service: Service) -> List[str]: + lines: List[str] = [] + lines.append(f"type {self.to_camel_case(service.name)}Client struct {{") + lines.append("\tcc grpc.ClientConnInterface") + lines.append("\tfory *fory.Fory") + lines.append("}") + lines.append("") + return lines + + def _generate_new_client(self, service: Service) -> List[str]: + lines: List[str] = [] + lines.append( + f"func New{service.name}Client(cc grpc.ClientConnInterface, f *fory.Fory) {service.name}Client {{" Review Comment: Generated model files already own a package-level `getFory() *threadsafe.Fory` with local and imported schema registrations installed. Requiring every generated client and `CodecV2` to receive a separate `*fory.Fory` splits that owner model, makes users duplicate generated registration, and is unsafe for concurrent RPCs because plain Go `Fory` reuses internal buffers and state. The gRPC companion should reuse the generated package runtime and make the codec/client constructor stateless from the user's perspective. ########## compiler/fory_compiler/generators/services/go.py: ########## @@ -0,0 +1,720 @@ +# 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. + +"""Go gRPC service code generator.""" + +from typing import List +from fory_compiler.generators.services.base import ( + ImportTracker, + StreamingMode, + streaming_mode, +) +from fory_compiler.generators.base import GeneratedFile +from fory_compiler.ir.ast import Service, NamedType + + +class GoServiceGeneratorMixin: + """Generates Go gRPC service stubs.""" + + def generate_services(self) -> List[GeneratedFile]: + local_services = [ + s for s in self.schema.services if not self.is_imported_type(s) + ] + if not local_services: + return [] + return [self._generate_grpc_file(local_services)] + + def _generate_grpc_file(self, services: List[Service]) -> GeneratedFile: + """Generate one _grpc.go file containing all services in the schema.""" + lines: List[str] = [] + tracker = ImportTracker() + + # License header + lines.append(self.get_license_header("//")) + lines.append("") + + # Package declaration + lines.append(f"package {self.get_package_name()}") + lines.append("") + + # Save placeholder index so imports can be inserted after all code is + # generated (imports are collected lazily via tracker as types are resolved). + import_placeholder_index = len(lines) + + # CodecV2 is emitted once per file, shared by all services in the schema. + lines.extend(self._generate_codec()) + + for service in services: + lines.extend(self._generate_client_interface(service, tracker)) + lines.extend(self._generate_client_struct(service)) + lines.extend(self._generate_new_client(service)) + lines.extend(self._generate_client_methods(service, tracker)) + lines.extend(self._generate_stream_types(service, tracker)) + lines.extend(self._generate_server_interface(service, tracker)) + lines.extend(self._generate_unimplemented_server(service, tracker)) + lines.extend(self._generate_server_stream_types(service, tracker)) + lines.extend(self._generate_service_desc(service, tracker)) + lines.extend(self._generate_register_server(service)) + + import_lines = self._build_import_block(tracker) + for i, line in enumerate(import_lines): + lines.insert(import_placeholder_index + i, line) + + return GeneratedFile( + path=f"{self.get_file_name()}_grpc.go", content="\n".join(lines) + ) + + def _build_import_block(self, tracker: ImportTracker) -> List[str]: + imports = [ + '"context"', + '"google.golang.org/grpc"', + '"google.golang.org/grpc/codes"', + '"google.golang.org/grpc/mem"', + '"google.golang.org/grpc/status"', + '"github.com/apache/fory/go/fory"', + ] + + for alias, path in tracker._imports.items(): + imports.append(f'{alias} "{path}"') + + sorted_imports = sorted(set(imports)) + + lines = ["import ("] + for imp in sorted_imports: + lines.append(f"\t{imp}") + lines.append(")") + lines.append("") + + return lines + + def _resolve_go_type(self, named_type: NamedType, tracker: ImportTracker) -> str: + type_ref = self.schema.resolve_type_name(named_type.name) + type_def = self.schema.get_type(type_ref) + if type_def is not None and self.is_imported_type(type_def): + info = self._import_info_for_type(type_def) + if info: + alias, import_path, _ = info + tracker.add(alias, import_path) + return f"*{alias}.{type_ref}" + return f"*{type_ref}" + + def _generate_client_interface( + self, service: Service, tracker: ImportTracker + ) -> List[str]: + lines: List[str] = [] + lines.append( + f"// {service.name}Client is the client API for {service.name} service." + ) + lines.append(f"type {service.name}Client interface {{") + for method in service.methods: + req_type = self._resolve_go_type(method.request_type, tracker) + res_type = self._resolve_go_type(method.response_type, tracker) + mode = streaming_mode(method) + if mode is StreamingMode.UNARY: + signature = f"(ctx context.Context, in {req_type}, opts ...grpc.CallOption) ({res_type}, error)" + lines.append(f"\t{self.to_pascal_case(method.name)}{signature}") Review Comment: This emits Go identifiers from `to_pascal_case(method.name)` without checking for collisions. A service with `rpc Foo` and `rpc foo` passes validation but generates duplicate `Foo` methods, duplicate handlers, and an uncompilable `_grpc.go` file. Add a Go-specific service/method collision check before emitting code, similar to the collision checks already present for the other generated gRPC targets. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
