From b9835c32cb4b9ff723af39859e723dc9200f0a85 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 11:22:12 +0530 Subject: [PATCH 01/17] added RPCMethod and Service Dataclass and updated the schema --- compiler/fory_compiler/frontend/fdl/lexer.py | 8 ++++ compiler/fory_compiler/ir/ast.py | 45 +++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/compiler/fory_compiler/frontend/fdl/lexer.py b/compiler/fory_compiler/frontend/fdl/lexer.py index 8bd1608494..6043de4960 100644 --- a/compiler/fory_compiler/frontend/fdl/lexer.py +++ b/compiler/fory_compiler/frontend/fdl/lexer.py @@ -44,6 +44,10 @@ class TokenType(Enum): RESERVED = auto() TO = auto() MAX = auto() + SERVICE = auto() + RPC = auto() + RETURNS = auto() + STREAM = auto() # Literals IDENT = auto() @@ -112,6 +116,10 @@ class Lexer: "reserved": TokenType.RESERVED, "to": TokenType.TO, "max": TokenType.MAX, + "service": TokenType.SERVICE, + "rpc": TokenType.RPC, + "returns": TokenType.RETURNS, + "stream": TokenType.STREAM, } PUNCTUATION = { diff --git a/compiler/fory_compiler/ir/ast.py b/compiler/fory_compiler/ir/ast.py index 4a1f2296bf..0c1d750099 100644 --- a/compiler/fory_compiler/ir/ast.py +++ b/compiler/fory_compiler/ir/ast.py @@ -245,6 +245,47 @@ def __repr__(self) -> str: return f"Union({self.name}{id_str}, fields={self.fields}{opts_str})" +@dataclass +class RpcMethod: + """An RPC method inside a service.""" + + name: str + request_type: NamedType + response_type: NamedType + client_streaming: bool = False + server_streaming: bool = False + options: dict = field(default_factory=dict) + line: int = 0 + column: int = 0 + location: Optional[SourceLocation] = None + + def __repr__(self) -> str: + opts_str = f" [{self.options}]" if self.options else "" + req_stream = "stream " if self.client_streaming else "" + res_stream = "stream " if self.server_streaming else "" + return ( + f"RpcMethod({self.name} " + f"({req_stream}{self.request_type}) returns ({res_stream}{self.response_type})" + f"{opts_str})" + ) + + +@dataclass +class Service: + """A service definition.""" + + name: str + methods: List[RpcMethod] = field(default_factory=list) + options: dict = field(default_factory=dict) + line: int = 0 + column: int = 0 + location: Optional[SourceLocation] = None + + def __repr__(self) -> str: + opts_str = f", options={len(self.options)}" if self.options else "" + return f"Service({self.name}, methods={len(self.methods)}{opts_str})" + + @dataclass class Schema: """The root AST node representing a complete FDL file.""" @@ -255,6 +296,7 @@ class Schema: enums: List[Enum] = field(default_factory=list) messages: List[Message] = field(default_factory=list) unions: List[Union] = field(default_factory=list) + services: List[Service] = field(default_factory=list) options: dict = field( default_factory=dict ) # File-level options (java_package, go_package, etc.) @@ -266,7 +308,8 @@ def __repr__(self) -> str: alias = f", package_alias={self.package_alias}" if self.package_alias else "" return ( f"Schema(package={self.package}{alias}, imports={len(self.imports)}, " - f"enums={len(self.enums)}, messages={len(self.messages)}, unions={len(self.unions)}{opts})" + f"enums={len(self.enums)}, messages={len(self.messages)}, unions={len(self.unions)}, " + f"services={len(self.services)}{opts})" ) def get_option(self, name: str, default: Optional[str] = None) -> Optional[str]: From e8c19e800a0cb099c2ff6980135ace3bdbbe4c32 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 11:32:14 +0530 Subject: [PATCH 02/17] added the service and rpc_method parser --- compiler/fory_compiler/frontend/fdl/parser.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/compiler/fory_compiler/frontend/fdl/parser.py b/compiler/fory_compiler/frontend/fdl/parser.py index f2caf1be40..1c326a2707 100644 --- a/compiler/fory_compiler/frontend/fdl/parser.py +++ b/compiler/fory_compiler/frontend/fdl/parser.py @@ -25,6 +25,8 @@ Message, Enum, Union, + Service, + RpcMethod, Field, EnumValue, Import, @@ -107,6 +109,14 @@ "deprecated", } +KNOWN_SERVICE_OPTIONS: Set[str] = { + "deprecated", +} + +KNOWN_METHOD_OPTIONS: Set[str] = { + "deprecated", +} + class ParseError(Exception): """Error during parsing.""" @@ -196,6 +206,7 @@ def parse(self) -> Schema: enums = [] messages = [] unions = [] + services = [] options = {} while not self.at_end(): @@ -215,6 +226,8 @@ def parse(self) -> Schema: unions.append(self.parse_union()) elif self.check(TokenType.MESSAGE): messages.append(self.parse_message()) + elif self.check(TokenType.SERVICE): + services.append(self.parse_service()) else: raise self.error(f"Unexpected token: {self.current().value}") @@ -225,6 +238,7 @@ def parse(self) -> Schema: enums=enums, messages=messages, unions=unions, + services=services, options=options, source_file=self.filename, source_format=self.source_format, @@ -822,6 +836,113 @@ def merge_ref_options( if thread_safe is not None: target["thread_safe_pointer"] = thread_safe + def parse_service(self) -> Service: + """Parse a service definition: service Greeter { rpc ... }""" + start = self.current() + self.consume(TokenType.SERVICE) + name = self.consume(TokenType.IDENT, "Expected service name").value + self.consume(TokenType.LBRACE, "Expected '{' after service name") + + methods = [] + options = {} + + while not self.check(TokenType.RBRACE): + if self.check(TokenType.OPTION): + # Service-level option + name_token = self.current() + opt_name, opt_value = self.parse_file_option() # Reusing generic option parser + options[opt_name] = opt_value + if opt_name not in KNOWN_SERVICE_OPTIONS: + warnings.warn( + f"Line {name_token.line}: ignoring unknown service option '{opt_name}'", + stacklevel=2, + ) + elif self.check(TokenType.RPC): + methods.append(self.parse_rpc_method()) + else: + raise self.error("Expected 'rpc' or 'option' inside service block") + + self.consume(TokenType.RBRACE, "Expected '}' after service body") + + return Service( + name=name, + methods=methods, + options=options, + line=start.line, + column=start.column, + location=self.make_location(start), + ) + + def parse_rpc_method(self) -> RpcMethod: + """Parse an RPC method: rpc Name (stream? Req) returns (stream? Res) { option ... };""" + start = self.current() + self.consume(TokenType.RPC) + name = self.consume(TokenType.IDENT, "Expected method name").value + + # Parse request type + self.consume(TokenType.LPAREN, "Expected '(' before request type") + client_streaming = False + if self.check(TokenType.STREAM): + self.advance() + client_streaming = True + + req_type_token = self.consume(TokenType.IDENT, "Expected request message type") + request_type = NamedType( + name=req_type_token.value, + location=self.make_location(req_type_token) + ) + self.consume(TokenType.RPAREN, "Expected ')' after request type") + + # Parse return type + self.consume(TokenType.RETURNS, "Expected 'returns' keyword") + self.consume(TokenType.LPAREN, "Expected '(' before response type") + server_streaming = False + if self.check(TokenType.STREAM): + self.advance() + server_streaming = True + + res_type_token = self.consume(TokenType.IDENT, "Expected response message type") + response_type = NamedType( + name=res_type_token.value, + location=self.make_location(res_type_token) + ) + self.consume(TokenType.RPAREN, "Expected ')' after response type") + + # Parse optional method options block + options = {} + if self.check(TokenType.LBRACE): + self.consume(TokenType.LBRACE) + while not self.check(TokenType.RBRACE): + if self.check(TokenType.OPTION): + name_token = self.current() + opt_name, opt_value = self.parse_file_option() + options[opt_name] = opt_value + if opt_name not in KNOWN_METHOD_OPTIONS: + warnings.warn( + f"Line {name_token.line}: ignoring unknown method option '{opt_name}'", + stacklevel=2, + ) + elif self.check(TokenType.SEMI): + # Allow empty ; inside block + self.advance() + else: + raise self.error("Expected 'option' inside method block") + self.consume(TokenType.RBRACE, "Expected '}' after method options") + else: + self.consume(TokenType.SEMI, "Expected ';' after method definition") + + return RpcMethod( + name=name, + request_type=request_type, + response_type=response_type, + client_streaming=client_streaming, + server_streaming=server_streaming, + options=options, + line=start.line, + column=start.column, + location=self.make_location(start), + ) + def parse_type_options( self, type_name: str, known_options: Set[str], allow_zero_id: bool = False ) -> dict: From bf96d0fda45e06daed87ff8009b94e3b6b2da005 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 11:48:31 +0530 Subject: [PATCH 03/17] ci code style checks --- compiler/fory_compiler/frontend/fdl/parser.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/fory_compiler/frontend/fdl/parser.py b/compiler/fory_compiler/frontend/fdl/parser.py index 1c326a2707..79c568f0ad 100644 --- a/compiler/fory_compiler/frontend/fdl/parser.py +++ b/compiler/fory_compiler/frontend/fdl/parser.py @@ -850,7 +850,9 @@ def parse_service(self) -> Service: if self.check(TokenType.OPTION): # Service-level option name_token = self.current() - opt_name, opt_value = self.parse_file_option() # Reusing generic option parser + opt_name, opt_value = ( + self.parse_file_option() + ) # Reusing generic option parser options[opt_name] = opt_value if opt_name not in KNOWN_SERVICE_OPTIONS: warnings.warn( @@ -885,11 +887,10 @@ def parse_rpc_method(self) -> RpcMethod: if self.check(TokenType.STREAM): self.advance() client_streaming = True - + req_type_token = self.consume(TokenType.IDENT, "Expected request message type") request_type = NamedType( - name=req_type_token.value, - location=self.make_location(req_type_token) + name=req_type_token.value, location=self.make_location(req_type_token) ) self.consume(TokenType.RPAREN, "Expected ')' after request type") @@ -903,8 +904,7 @@ def parse_rpc_method(self) -> RpcMethod: res_type_token = self.consume(TokenType.IDENT, "Expected response message type") response_type = NamedType( - name=res_type_token.value, - location=self.make_location(res_type_token) + name=res_type_token.value, location=self.make_location(res_type_token) ) self.consume(TokenType.RPAREN, "Expected ')' after response type") @@ -918,13 +918,13 @@ def parse_rpc_method(self) -> RpcMethod: opt_name, opt_value = self.parse_file_option() options[opt_name] = opt_value if opt_name not in KNOWN_METHOD_OPTIONS: - warnings.warn( + warnings.warn( f"Line {name_token.line}: ignoring unknown method option '{opt_name}'", stacklevel=2, ) elif self.check(TokenType.SEMI): - # Allow empty ; inside block - self.advance() + # Allow empty ; inside block + self.advance() else: raise self.error("Expected 'option' inside method block") self.consume(TokenType.RBRACE, "Expected '}' after method options") From 2845add1aee03832a4c4d65bc7d4d05fb97ecdd4 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 11:55:05 +0530 Subject: [PATCH 04/17] added tests --- .../fory_compiler/tests/test_fdl_service.py | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 compiler/fory_compiler/tests/test_fdl_service.py diff --git a/compiler/fory_compiler/tests/test_fdl_service.py b/compiler/fory_compiler/tests/test_fdl_service.py new file mode 100644 index 0000000000..4f4c0b0ed1 --- /dev/null +++ b/compiler/fory_compiler/tests/test_fdl_service.py @@ -0,0 +1,162 @@ +# 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 pytest +from fory_compiler.frontend.fdl.parser import Parser, ParseError +from fory_compiler.ir.ast import Service, RpcMethod + +def parse(source: str): + parser = Parser.from_source(source) + schema = parser.parse() + return schema + +def test_empty_service(): + source = """ + package test; + service Greeter {} + """ + schema = parse(source) + assert len(schema.services) == 1 + service = schema.services[0] + assert service.name == "Greeter" + assert len(service.methods) == 0 + +def test_unary_rpc(): + source = """ + package test; + + message HelloRequest {} + message HelloReply {} + + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); + } + """ + schema = parse(source) + service = schema.services[0] + assert len(service.methods) == 1 + method = service.methods[0] + assert method.name == "SayHello" + assert method.request_type.name == "HelloRequest" + assert method.response_type.name == "HelloReply" + assert not method.client_streaming + assert not method.server_streaming + +def test_client_streaming_rpc(): + source = """ + package test; + + message HelloRequest {} + message HelloReply {} + + service Greeter { + rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply); + } + """ + schema = parse(source) + service = schema.services[0] + method = service.methods[0] + assert method.name == "LotsOfGreetings" + assert method.client_streaming + assert not method.server_streaming + +def test_server_streaming_rpc(): + source = """ + package test; + + message HelloRequest {} + message HelloReply {} + + service Greeter { + rpc LotsOfReplies (HelloRequest) returns (stream HelloReply); + } + """ + schema = parse(source) + service = schema.services[0] + method = service.methods[0] + assert method.name == "LotsOfReplies" + assert not method.client_streaming + assert method.server_streaming + +def test_bidi_streaming_rpc(): + source = """ + package test; + + message HelloRequest {} + message HelloReply {} + + service Greeter { + rpc BidiHello (stream HelloRequest) returns (stream HelloReply); + } + """ + schema = parse(source) + service = schema.services[0] + method = service.methods[0] + assert method.name == "BidiHello" + assert method.client_streaming + assert method.server_streaming + +def test_service_options(): + source = """ + package test; + + service Greeter { + option deprecated = true; + } + """ + schema = parse(source) + service = schema.services[0] + assert service.options["deprecated"] is True + +def test_method_options(): + source = """ + package test; + + message HelloRequest {} + message HelloReply {} + + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) { + option deprecated = true; + } + } + """ + schema = parse(source) + service = schema.services[0] + method = service.methods[0] + assert method.options["deprecated"] is True + +def test_invalid_syntax_missing_returns(): + source = """ + package test; + service Greeter { + rpc SayHello (HelloRequest); + } + """ + with pytest.raises(ParseError): + parse(source) + +def test_invalid_syntax_missing_parens(): + source = """ + package test; + service Greeter { + rpc SayHello HelloRequest returns HelloReply; + } + """ + with pytest.raises(ParseError): + parse(source) + From 7f1bdf36b5e2bfbc408f8c93db8ab86caf88fd44 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 12:01:26 +0530 Subject: [PATCH 05/17] ci tests --- compiler/fory_compiler/tests/test_fdl_service.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/fory_compiler/tests/test_fdl_service.py b/compiler/fory_compiler/tests/test_fdl_service.py index 4f4c0b0ed1..a884c98200 100644 --- a/compiler/fory_compiler/tests/test_fdl_service.py +++ b/compiler/fory_compiler/tests/test_fdl_service.py @@ -17,13 +17,14 @@ import pytest from fory_compiler.frontend.fdl.parser import Parser, ParseError -from fory_compiler.ir.ast import Service, RpcMethod + def parse(source: str): parser = Parser.from_source(source) schema = parser.parse() return schema + def test_empty_service(): source = """ package test; @@ -35,6 +36,7 @@ def test_empty_service(): assert service.name == "Greeter" assert len(service.methods) == 0 + def test_unary_rpc(): source = """ package test; @@ -56,6 +58,7 @@ def test_unary_rpc(): assert not method.client_streaming assert not method.server_streaming + def test_client_streaming_rpc(): source = """ package test; @@ -74,6 +77,7 @@ def test_client_streaming_rpc(): assert method.client_streaming assert not method.server_streaming + def test_server_streaming_rpc(): source = """ package test; @@ -92,6 +96,7 @@ def test_server_streaming_rpc(): assert not method.client_streaming assert method.server_streaming + def test_bidi_streaming_rpc(): source = """ package test; @@ -110,6 +115,7 @@ def test_bidi_streaming_rpc(): assert method.client_streaming assert method.server_streaming + def test_service_options(): source = """ package test; @@ -122,6 +128,7 @@ def test_service_options(): service = schema.services[0] assert service.options["deprecated"] is True + def test_method_options(): source = """ package test; @@ -140,6 +147,7 @@ def test_method_options(): method = service.methods[0] assert method.options["deprecated"] is True + def test_invalid_syntax_missing_returns(): source = """ package test; @@ -150,6 +158,7 @@ def test_invalid_syntax_missing_returns(): with pytest.raises(ParseError): parse(source) + def test_invalid_syntax_missing_parens(): source = """ package test; @@ -159,4 +168,3 @@ def test_invalid_syntax_missing_parens(): """ with pytest.raises(ParseError): parse(source) - From 895cded79d5e6edb0cbc80494475dfacb07918af Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 7 Feb 2026 19:59:23 +0530 Subject: [PATCH 06/17] trigger ci From 827751390e61eb339bc696a222ff43aa9f6ee8dd Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 11:54:52 +0530 Subject: [PATCH 07/17] added fbsRpcMethod fbsService dataclass and updated the schema --- compiler/fory_compiler/frontend/fbs/ast.py | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/compiler/fory_compiler/frontend/fbs/ast.py b/compiler/fory_compiler/frontend/fbs/ast.py index 17e0f4c1a0..ad3bde9474 100644 --- a/compiler/fory_compiler/frontend/fbs/ast.py +++ b/compiler/fory_compiler/frontend/fbs/ast.py @@ -109,6 +109,29 @@ class FbsStruct: column: int = 0 +@dataclass +class FbsRpcMethod: + """An RPC method declaration.""" + + name: str + request_type: str + response_type: str + attributes: Dict[str, object] = field(default_factory=dict) + line: int = 0 + column: int = 0 + + +@dataclass +class FbsService: + """A FlatBuffers service declaration.""" + + name: str + methods: List[FbsRpcMethod] = field(default_factory=list) + attributes: Dict[str, object] = field(default_factory=dict) + line: int = 0 + column: int = 0 + + @dataclass class FbsSchema: """The root node representing a FlatBuffers schema.""" @@ -120,5 +143,6 @@ class FbsSchema: unions: List[FbsUnion] = field(default_factory=list) tables: List[FbsTable] = field(default_factory=list) structs: List[FbsStruct] = field(default_factory=list) + services: List[FbsService] = field(default_factory=list) root_type: Optional[str] = None source_file: Optional[str] = None From dc807b2ca59f3a7466d030c70e4c2d80cdfff7fd Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 11:57:05 +0530 Subject: [PATCH 08/17] reserved the keywords in lexer --- compiler/fory_compiler/frontend/fbs/lexer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compiler/fory_compiler/frontend/fbs/lexer.py b/compiler/fory_compiler/frontend/fbs/lexer.py index f70b5a95a9..a392ab983b 100644 --- a/compiler/fory_compiler/frontend/fbs/lexer.py +++ b/compiler/fory_compiler/frontend/fbs/lexer.py @@ -38,6 +38,10 @@ class TokenType(Enum): FILE_EXTENSION = auto() TRUE = auto() FALSE = auto() + SERVICE = auto() + RPC = auto() + RETURNS = auto() + STREAM = auto() # Literals IDENT = auto() @@ -97,6 +101,11 @@ class Lexer: "file_extension": TokenType.FILE_EXTENSION, "true": TokenType.TRUE, "false": TokenType.FALSE, + "service": TokenType.SERVICE, + "rpc_service": TokenType.SERVICE, + "rpc": TokenType.RPC, + "returns": TokenType.RETURNS, + "stream": TokenType.STREAM, } PUNCTUATION = { From ae854fdcb9aa90a468a9f1a485987533379a1a05 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 11:57:51 +0530 Subject: [PATCH 09/17] added parser defs --- compiler/fory_compiler/frontend/fbs/parser.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/compiler/fory_compiler/frontend/fbs/parser.py b/compiler/fory_compiler/frontend/fbs/parser.py index 5c84e008e7..0869ffa915 100644 --- a/compiler/fory_compiler/frontend/fbs/parser.py +++ b/compiler/fory_compiler/frontend/fbs/parser.py @@ -29,7 +29,11 @@ FbsUnion, FbsTypeName, FbsTypeRef, + FbsTypeName, + FbsTypeRef, FbsVectorType, + FbsService, + FbsRpcMethod, ) from fory_compiler.frontend.fbs.lexer import Token, TokenType @@ -96,7 +100,9 @@ def parse(self) -> FbsSchema: enums: List[FbsEnum] = [] unions: List[FbsUnion] = [] tables: List[FbsTable] = [] + tables: List[FbsTable] = [] structs: List[FbsStruct] = [] + services: List[FbsService] = [] root_type: Optional[str] = None while not self.at_end(): @@ -122,6 +128,8 @@ def parse(self) -> FbsSchema: self.parse_file_extension() elif self.check(TokenType.UNION): unions.append(self.parse_union()) + elif self.check(TokenType.SERVICE) or self.check(TokenType.RPC): + services.append(self.parse_service()) elif self.check(TokenType.SEMI): self.advance() else: @@ -135,6 +143,7 @@ def parse(self) -> FbsSchema: unions=unions, tables=tables, structs=structs, + services=services, root_type=root_type, source_file=self.filename, ) @@ -369,7 +378,11 @@ def parse_value(self) -> object: if self.match(TokenType.FALSE): return False if self.check(TokenType.INT): - return int(self.advance().value, 0) + value = self.advance().value + try: + return int(value, 0) + except ValueError: + return int(value) if self.check(TokenType.FLOAT): return float(self.advance().value) if self.check(TokenType.STRING): @@ -377,3 +390,65 @@ def parse_value(self) -> object: if self.check(TokenType.IDENT): return self.advance().value raise self.error("Expected value") + + def parse_service(self) -> FbsService: + start = self.current() + # Support both 'service' and 'rpc_service' keywords for defining services. + if self.check(TokenType.SERVICE): + self.advance() + elif self.check(TokenType.RPC): + # 'rpc_service' is mapped to TokenType.SERVICE in the lexer, + # but we also check for separate 'rpc' token just in case. + self.advance() + if self.check(TokenType.SERVICE): + self.advance() + else: + raise self.error("Expected 'service' or 'rpc_service'") + + name = self.consume(TokenType.IDENT, "Expected service name").value + attributes = self.parse_metadata() + self.consume(TokenType.LBRACE, "Expected '{' after service name") + + methods: List[FbsRpcMethod] = [] + while not self.check(TokenType.RBRACE): + if self.check(TokenType.SEMI): + self.advance() + continue + methods.append(self.parse_rpc_method()) + + self.consume(TokenType.RBRACE, "Expected '}' after service body") + if self.check(TokenType.SEMI): + self.advance() + + return FbsService( + name=name, + methods=methods, + attributes=attributes, + line=start.line, + column=start.column, + ) + + def parse_rpc_method(self) -> FbsRpcMethod: + # Parse method signature: name(RequestType):ResponseType; + start = self.current() + name = self.consume(TokenType.IDENT, "Expected method name").value + + self.consume(TokenType.LPAREN, "Expected '(' after method name") + # Parsing request type. FBS allows type name here. + req_type = self.parse_qualified_ident() + self.consume(TokenType.RPAREN, "Expected ')' after request type") + + self.consume(TokenType.COLON, "Expected ':' before response type") + res_type = self.parse_qualified_ident() + + attributes = self.parse_metadata() + self.consume(TokenType.SEMI, "Expected ';' after method declaration") + + return FbsRpcMethod( + name=name, + request_type=req_type, + response_type=res_type, + attributes=attributes, + line=start.line, + column=start.column, + ) From 1052644b1900f400bce8d41d2f87bad5fb9965ea Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 11:58:55 +0530 Subject: [PATCH 10/17] added translator for service and rpmmethod --- .../fory_compiler/frontend/fbs/translator.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/compiler/fory_compiler/frontend/fbs/translator.py b/compiler/fory_compiler/frontend/fbs/translator.py index c42ce0bfc3..15a0b8d242 100644 --- a/compiler/fory_compiler/frontend/fbs/translator.py +++ b/compiler/fory_compiler/frontend/fbs/translator.py @@ -27,7 +27,12 @@ FbsTypeName, FbsTypeRef, FbsVectorType, + FbsTypeName, + FbsTypeRef, + FbsVectorType, FbsUnion, + FbsService, + FbsRpcMethod, ) from fory_compiler.ir.ast import ( Enum, @@ -39,6 +44,11 @@ NamedType, PrimitiveType, Schema, + NamedType, + PrimitiveType, + Schema, + Service, + RpcMethod, SourceLocation, Union, ) @@ -82,6 +92,7 @@ def translate(self) -> Schema: enums=[self._translate_enum(e) for e in self.schema.enums], unions=[self._translate_union(u) for u in self.schema.unions], messages=self._translate_messages(), + services=[self._translate_service(s) for s in self.schema.services], options={}, source_file=self.schema.source_file, source_format="fbs", @@ -278,3 +289,49 @@ def _translate_type(self, fbs_type: FbsTypeRef): fbs_type.name, location=self._location(fbs_type.line, fbs_type.column) ) raise ValueError("Unknown FlatBuffers type") + + def _translate_service(self, fbs_service: FbsService) -> Service: + return Service( + name=fbs_service.name, + methods=[self._translate_rpc_method(m) for m in fbs_service.methods], + options=dict(fbs_service.attributes), + line=fbs_service.line, + column=fbs_service.column, + location=self._location(fbs_service.line, fbs_service.column), + ) + + def _translate_rpc_method(self, fbs_method: FbsRpcMethod) -> RpcMethod: + # Map FBS 'streaming' attribute to Fory's client_streaming/server_streaming flags. + # Expected 'streaming' values: "client", "server", "bidi". + # Default is unary (no streaming). + + attributes = dict(fbs_method.attributes) + client_streaming = False + server_streaming = False + + # Check for streaming attributes if any (convention) + if attributes.get("streaming") == "client": + client_streaming = True + elif attributes.get("streaming") == "server": + server_streaming = True + elif attributes.get("streaming") == "bidi": + client_streaming = True + server_streaming = True + + return RpcMethod( + name=fbs_method.name, + request_type=NamedType( + name=fbs_method.request_type, + location=self._location(fbs_method.line, fbs_method.column), + ), + response_type=NamedType( + name=fbs_method.response_type, + location=self._location(fbs_method.line, fbs_method.column), + ), + client_streaming=client_streaming, + server_streaming=server_streaming, + options=attributes, + line=fbs_method.line, + column=fbs_method.column, + location=self._location(fbs_method.line, fbs_method.column), + ) From 891e6591b497ec5cc19e69406fd915a228431ad4 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 11:59:26 +0530 Subject: [PATCH 11/17] added tests suites --- .../fory_compiler/tests/test_fbs_service.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 compiler/fory_compiler/tests/test_fbs_service.py diff --git a/compiler/fory_compiler/tests/test_fbs_service.py b/compiler/fory_compiler/tests/test_fbs_service.py new file mode 100644 index 0000000000..b61f194c13 --- /dev/null +++ b/compiler/fory_compiler/tests/test_fbs_service.py @@ -0,0 +1,109 @@ +# 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. + +"""Tests for FBS service parsing.""" + +from fory_compiler.frontend.fbs.lexer import Lexer +from fory_compiler.frontend.fbs.parser import Parser +from fory_compiler.frontend.fbs.translator import FbsTranslator + + +def parse_and_translate(source): + lexer = Lexer(source) + parser = Parser(lexer.tokenize()) + schema = parser.parse() + translator = FbsTranslator(schema) + return translator.translate() + + +def test_rpc_service_parsing(): + source = """ + namespace demo; + + table Request { + id: int; + } + + table Response { + result: string; + } + + rpc_service Greeter { + SayHello(Request):Response; + SayGoodbye(Request):Response (deprecated); + } + """ + schema = parse_and_translate(source) + assert len(schema.services) == 1 + service = schema.services[0] + assert service.name == "Greeter" + assert len(service.methods) == 2 + + assert service.methods[0].name == "SayHello" + assert service.methods[0].request_type.name == "Request" + assert service.methods[0].response_type.name == "Response" + assert not service.methods[0].client_streaming + assert not service.methods[0].server_streaming + + assert service.methods[1].name == "SayGoodbye" + assert service.methods[1].options["deprecated"] is True + + +def test_service_keyword_parsing(): + """Test using 'service' keyword instead of 'rpc_service'.""" + source = """ + namespace demo; + + service Greeter { + SayHello(Request):Response; + } + """ + schema = parse_and_translate(source) + assert len(schema.services) == 1 + assert schema.services[0].name == "Greeter" + + +def test_streaming_attributes(): + source = """ + namespace demo; + + rpc_service Streamer { + ClientStream(Request):Response (streaming: "client"); + ServerStream(Request):Response (streaming: "server"); + BidiStream(Request):Response (streaming: "bidi"); + } + """ + schema = parse_and_translate(source) + service = schema.services[0] + + # Client streaming + m1 = service.methods[0] + assert m1.name == "ClientStream" + assert m1.client_streaming is True + assert m1.server_streaming is False + + # Server streaming + m2 = service.methods[1] + assert m2.name == "ServerStream" + assert m2.client_streaming is False + assert m2.server_streaming is True + + # Bidi streaming + m3 = service.methods[2] + assert m3.name == "BidiStream" + assert m3.client_streaming is True + assert m3.server_streaming is True From 63d8f55c4838e5c265aa693fa6ed520405f790ef Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 12:03:29 +0530 Subject: [PATCH 12/17] ci checks --- compiler/fory_compiler/frontend/fbs/parser.py | 10 ++++------ compiler/fory_compiler/frontend/fbs/translator.py | 12 +++--------- compiler/fory_compiler/tests/test_fbs_service.py | 6 +++--- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/compiler/fory_compiler/frontend/fbs/parser.py b/compiler/fory_compiler/frontend/fbs/parser.py index 0869ffa915..67013c26e8 100644 --- a/compiler/fory_compiler/frontend/fbs/parser.py +++ b/compiler/fory_compiler/frontend/fbs/parser.py @@ -29,8 +29,6 @@ FbsUnion, FbsTypeName, FbsTypeRef, - FbsTypeName, - FbsTypeRef, FbsVectorType, FbsService, FbsRpcMethod, @@ -432,18 +430,18 @@ def parse_rpc_method(self) -> FbsRpcMethod: # Parse method signature: name(RequestType):ResponseType; start = self.current() name = self.consume(TokenType.IDENT, "Expected method name").value - + self.consume(TokenType.LPAREN, "Expected '(' after method name") # Parsing request type. FBS allows type name here. req_type = self.parse_qualified_ident() self.consume(TokenType.RPAREN, "Expected ')' after request type") - + self.consume(TokenType.COLON, "Expected ':' before response type") res_type = self.parse_qualified_ident() - + attributes = self.parse_metadata() self.consume(TokenType.SEMI, "Expected ';' after method declaration") - + return FbsRpcMethod( name=name, request_type=req_type, diff --git a/compiler/fory_compiler/frontend/fbs/translator.py b/compiler/fory_compiler/frontend/fbs/translator.py index 15a0b8d242..677cf14204 100644 --- a/compiler/fory_compiler/frontend/fbs/translator.py +++ b/compiler/fory_compiler/frontend/fbs/translator.py @@ -27,9 +27,6 @@ FbsTypeName, FbsTypeRef, FbsVectorType, - FbsTypeName, - FbsTypeRef, - FbsVectorType, FbsUnion, FbsService, FbsRpcMethod, @@ -44,9 +41,6 @@ NamedType, PrimitiveType, Schema, - NamedType, - PrimitiveType, - Schema, Service, RpcMethod, SourceLocation, @@ -304,11 +298,11 @@ def _translate_rpc_method(self, fbs_method: FbsRpcMethod) -> RpcMethod: # Map FBS 'streaming' attribute to Fory's client_streaming/server_streaming flags. # Expected 'streaming' values: "client", "server", "bidi". # Default is unary (no streaming). - + attributes = dict(fbs_method.attributes) client_streaming = False server_streaming = False - + # Check for streaming attributes if any (convention) if attributes.get("streaming") == "client": client_streaming = True @@ -317,7 +311,7 @@ def _translate_rpc_method(self, fbs_method: FbsRpcMethod) -> RpcMethod: elif attributes.get("streaming") == "bidi": client_streaming = True server_streaming = True - + return RpcMethod( name=fbs_method.name, request_type=NamedType( diff --git a/compiler/fory_compiler/tests/test_fbs_service.py b/compiler/fory_compiler/tests/test_fbs_service.py index b61f194c13..27b86c7842 100644 --- a/compiler/fory_compiler/tests/test_fbs_service.py +++ b/compiler/fory_compiler/tests/test_fbs_service.py @@ -89,19 +89,19 @@ def test_streaming_attributes(): """ schema = parse_and_translate(source) service = schema.services[0] - + # Client streaming m1 = service.methods[0] assert m1.name == "ClientStream" assert m1.client_streaming is True assert m1.server_streaming is False - + # Server streaming m2 = service.methods[1] assert m2.name == "ServerStream" assert m2.client_streaming is False assert m2.server_streaming is True - + # Bidi streaming m3 = service.methods[2] assert m3.name == "BidiStream" From 4117e7f3e709761cc7808e5cb3d41f2d20cb206d Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 18:40:30 +0530 Subject: [PATCH 13/17] added dataclasses for proto ast compiler --- compiler/fory_compiler/frontend/proto/ast.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/compiler/fory_compiler/frontend/proto/ast.py b/compiler/fory_compiler/frontend/proto/ast.py index b881f467fa..7b81b0ed89 100644 --- a/compiler/fory_compiler/frontend/proto/ast.py +++ b/compiler/fory_compiler/frontend/proto/ast.py @@ -91,6 +91,31 @@ class ProtoMessage: column: int = 0 +@dataclass +class ProtoRpcMethod: + """ An RPC method declaration. """ + + name: str + request_type: str + response_type: str + client_streaming: bool = False + server_streaming: bool = False + options: Dict[str, object] = field(default_factory=dict) + line: int = 0 + column: int = 0 + + +@dataclass +class ProtoService: + """A ProtoBuffers service declaration.""" + + name: str + methods: List["ProtoRpcMethod"] = field(default_factory=list) + options: Dict[str, object] = field(default_factory=dict) + line: int = 0 + column: int = 0 + + @dataclass class ProtoSchema: """Represents a proto file.""" @@ -99,6 +124,7 @@ class ProtoSchema: package: Optional[str] imports: List[str] = field(default_factory=list) enums: List[ProtoEnum] = field(default_factory=list) + services: List[ProtoService] = field(default_factory=list) messages: List[ProtoMessage] = field(default_factory=list) options: Dict[str, object] = field(default_factory=dict) source_file: Optional[str] = None From b445289e40d4d37703411b4990203b665173b329 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 18:43:18 +0530 Subject: [PATCH 14/17] added stream keyword and the parse_service method --- .../fory_compiler/frontend/proto/lexer.py | 2 + .../fory_compiler/frontend/proto/parser.py | 96 ++++++++++++++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/compiler/fory_compiler/frontend/proto/lexer.py b/compiler/fory_compiler/frontend/proto/lexer.py index c8afe4e7c5..67a4c71fb4 100644 --- a/compiler/fory_compiler/frontend/proto/lexer.py +++ b/compiler/fory_compiler/frontend/proto/lexer.py @@ -48,6 +48,7 @@ class TokenType(Enum): FALSE = auto() TO = auto() MAX = auto() + STREAM = auto() # Literals IDENT = auto() @@ -120,6 +121,7 @@ class Lexer: "false": TokenType.FALSE, "to": TokenType.TO, "max": TokenType.MAX, + "stream": TokenType.STREAM, } PUNCTUATION = { diff --git a/compiler/fory_compiler/frontend/proto/parser.py b/compiler/fory_compiler/frontend/proto/parser.py index 127a5cad04..755da9788d 100644 --- a/compiler/fory_compiler/frontend/proto/parser.py +++ b/compiler/fory_compiler/frontend/proto/parser.py @@ -27,6 +27,8 @@ ProtoField, ProtoOneof, ProtoType, + ProtoService, + ProtoRpcMethod, ) from fory_compiler.frontend.proto.lexer import Token, TokenType @@ -92,6 +94,7 @@ def parse(self) -> ProtoSchema: imports: List[str] = [] enums: List[ProtoEnum] = [] messages: List[ProtoMessage] = [] + services: List[ProtoService] = [] options = {} while not self.at_end(): @@ -116,7 +119,7 @@ def parse(self) -> ProtoSchema: elif self.check(TokenType.ENUM): enums.append(self.parse_enum()) elif self.check(TokenType.SERVICE): - self.parse_service() + services.append(self.parse_service()) elif self.check(TokenType.SEMI): self.advance() else: @@ -133,6 +136,7 @@ def parse(self) -> ProtoSchema: imports=imports, enums=enums, messages=messages, + services=services, options=options, source_file=self.filename, ) @@ -392,20 +396,90 @@ def parse_extensions(self) -> None: self.advance() self.consume(TokenType.SEMI, "Expected ';' after extensions") - def parse_service(self) -> None: + def parse_service(self) -> ProtoService: + start = self.current() self.consume(TokenType.SERVICE, "Expected 'service'") - self.consume(TokenType.IDENT, "Expected service name") + name = self.consume(TokenType.IDENT, "Expected service name").value self.consume(TokenType.LBRACE, "Expected '{' after service name") - depth = 1 - while depth > 0: - if self.at_end(): - raise self.error("Unterminated service block") - if self.match(TokenType.LBRACE): - depth += 1 - elif self.match(TokenType.RBRACE): - depth -= 1 + + methods: List[ProtoRpcMethod] = [] + options = {} + + while not self.check(TokenType.RBRACE): + if self.check(TokenType.OPTION): + opt_name, opt_value = self.parse_option_statement() + options[opt_name] = opt_value + elif self.check(TokenType.RPC): + methods.append(self.parse_rpc_method()) + elif self.check(TokenType.SEMI): + self.advance() else: + raise self.error("Expected 'rpc' or 'option' inside service") + + self.consume(TokenType.RBRACE, "Expected '}' after service") + if self.check(TokenType.SEMI): + self.advance() + + return ProtoService( + name=name, + methods=methods, + options=options, + line=start.line, + column=start.column, + ) + + def parse_rpc_method(self) -> ProtoRpcMethod: + start = self.current() + self.consume(TokenType.RPC, "Expected 'rpc'") + name = self.consume(TokenType.IDENT, "Expected method name").value + + # Request + self.consume(TokenType.LPAREN, "Expected '(' before request type") + client_streaming = False + if self.match(TokenType.STREAM): + client_streaming = True + + req_type = self.parse_full_ident() + self.consume(TokenType.RPAREN, "Expected ')' after request type") + + self.consume(TokenType.RETURNS, "Expected 'returns'") + + # Response + self.consume(TokenType.LPAREN, "Expected '(' before response type") + server_streaming = False + if self.match(TokenType.STREAM): + server_streaming = True + + res_type = self.parse_full_ident() + self.consume(TokenType.RPAREN, "Expected ')' after response type") + + options = {} + if self.check(TokenType.LBRACE): + self.consume(TokenType.LBRACE, "Expected '{'") + while not self.check(TokenType.RBRACE): + if self.check(TokenType.OPTION): + opt_name, opt_value = self.parse_option_statement() + options[opt_name] = opt_value + elif self.check(TokenType.SEMI): + self.advance() + else: + raise self.error("Expected 'option' in method body") + self.consume(TokenType.RBRACE, "Expected '}'") + if self.check(TokenType.SEMI): self.advance() + else: + self.consume(TokenType.SEMI, "Expected ';' or '{' after method signature") + + return ProtoRpcMethod( + name=name, + request_type=req_type, + response_type=res_type, + client_streaming=client_streaming, + server_streaming=server_streaming, + options=options, + line=start.line, + column=start.column, + ) def parse_option_name(self) -> str: if self.match(TokenType.LPAREN): From cb1a2fb872793704bef754c584e3ae59d0fbb0c2 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 18:47:58 +0530 Subject: [PATCH 15/17] added defs to translate parse methods --- compiler/fory_compiler/frontend/proto/ast.py | 2 +- .../fory_compiler/frontend/proto/parser.py | 4 +- .../frontend/proto/translator.py | 40 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/compiler/fory_compiler/frontend/proto/ast.py b/compiler/fory_compiler/frontend/proto/ast.py index 7b81b0ed89..8b5ece33ff 100644 --- a/compiler/fory_compiler/frontend/proto/ast.py +++ b/compiler/fory_compiler/frontend/proto/ast.py @@ -93,7 +93,7 @@ class ProtoMessage: @dataclass class ProtoRpcMethod: - """ An RPC method declaration. """ + """An RPC method declaration.""" name: str request_type: str diff --git a/compiler/fory_compiler/frontend/proto/parser.py b/compiler/fory_compiler/frontend/proto/parser.py index 755da9788d..a5c69b4079 100644 --- a/compiler/fory_compiler/frontend/proto/parser.py +++ b/compiler/fory_compiler/frontend/proto/parser.py @@ -438,7 +438,7 @@ def parse_rpc_method(self) -> ProtoRpcMethod: client_streaming = False if self.match(TokenType.STREAM): client_streaming = True - + req_type = self.parse_full_ident() self.consume(TokenType.RPAREN, "Expected ')' after request type") @@ -449,7 +449,7 @@ def parse_rpc_method(self) -> ProtoRpcMethod: server_streaming = False if self.match(TokenType.STREAM): server_streaming = True - + res_type = self.parse_full_ident() self.consume(TokenType.RPAREN, "Expected ')' after response type") diff --git a/compiler/fory_compiler/frontend/proto/translator.py b/compiler/fory_compiler/frontend/proto/translator.py index 2828dc0e8e..00c5984337 100644 --- a/compiler/fory_compiler/frontend/proto/translator.py +++ b/compiler/fory_compiler/frontend/proto/translator.py @@ -26,9 +26,13 @@ ProtoField, ProtoType, ProtoOneof, + ProtoService, + ProtoRpcMethod, ) from fory_compiler.ir.ast import ( Schema, + Service, + RpcMethod, Message, Enum, Union, @@ -101,6 +105,7 @@ def translate(self) -> Schema: imports=self._translate_imports(), enums=[self._translate_enum(e) for e in self.proto_schema.enums], messages=[self._translate_message(m) for m in self.proto_schema.messages], + services=[self._translate_service(s) for s in self.proto_schema.services], options=self._translate_file_options(self.proto_schema.options), source_file=self.proto_schema.source_file, source_format="proto", @@ -318,6 +323,8 @@ def _translate_type_options( type_id = value elif name.startswith("fory."): translated[name.removeprefix("fory.")] = value + else: + translated[name] = value return type_id, translated def _translate_field_options( @@ -363,3 +370,36 @@ def _apply_type_override( if isinstance(field_type, PrimitiveType): return PrimitiveType(override, location=self._location(line, column)) raise ValueError("fory.type overrides are only supported for primitive fields") + + def _translate_service(self, proto_service: ProtoService) -> Service: + # Translate ProtoService to Service + _, options = self._translate_type_options(proto_service.options) + return Service( + name=proto_service.name, + methods=[self._translate_rpc_method(m) for m in proto_service.methods], + options=options, + line=proto_service.line, + column=proto_service.column, + location=self._location(proto_service.line, proto_service.column), + ) + + def _translate_rpc_method(self, proto_method: ProtoRpcMethod) -> RpcMethod: + # Translate ProtoRpcMethod to RpcMethod + _, options = self._translate_type_options(proto_method.options) + return RpcMethod( + name=proto_method.name, + request_type=NamedType( + name=proto_method.request_type, + location=self._location(proto_method.line, proto_method.column), + ), + response_type=NamedType( + name=proto_method.response_type, + location=self._location(proto_method.line, proto_method.column), + ), + client_streaming=proto_method.client_streaming, + server_streaming=proto_method.server_streaming, + options=options, + line=proto_method.line, + column=proto_method.column, + location=self._location(proto_method.line, proto_method.column), + ) From 083d364f0b6700038f2adc38d283b8d8846293e1 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 19:07:09 +0530 Subject: [PATCH 16/17] added tests --- .../fory_compiler/tests/test_proto_service.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 compiler/fory_compiler/tests/test_proto_service.py diff --git a/compiler/fory_compiler/tests/test_proto_service.py b/compiler/fory_compiler/tests/test_proto_service.py new file mode 100644 index 0000000000..c8bc7f91b2 --- /dev/null +++ b/compiler/fory_compiler/tests/test_proto_service.py @@ -0,0 +1,122 @@ +# 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. + +"""Tests for Proto service parsing.""" + +from fory_compiler.frontend.proto.lexer import Lexer +from fory_compiler.frontend.proto.parser import Parser +from fory_compiler.frontend.proto.translator import ProtoTranslator + + +def parse_and_translate(source): + lexer = Lexer(source) + parser = Parser(lexer.tokenize()) + schema = parser.parse() + translator = ProtoTranslator(schema) + return translator.translate() + + +def test_service_parsing(): + source = """ + syntax = "proto3"; + package demo; + + message Request { + int32 id = 1; + } + + message Response { + string result = 1; + } + + service Greeter { + rpc SayHello (Request) returns (Response); + rpc SayGoodbye (Request) returns (Response) { + option deprecated = true; + } + } + """ + schema = parse_and_translate(source) + assert len(schema.services) == 1 + service = schema.services[0] + assert service.name == "Greeter" + assert len(service.methods) == 2 + + m1 = service.methods[0] + assert m1.name == "SayHello" + assert m1.request_type.name == "Request" + assert m1.response_type.name == "Response" + assert not m1.client_streaming + assert not m1.server_streaming + + m2 = service.methods[1] + assert m2.name == "SayGoodbye" + assert m2.options["deprecated"] is True + + +def test_streaming_rpc(): + source = """ + syntax = "proto3"; + package demo; + + message Request {} + message Response {} + + service Streamer { + rpc ClientStream (stream Request) returns (Response); + rpc ServerStream (Request) returns (stream Response); + rpc BidiStream (stream Request) returns (stream Response); + } + """ + schema = parse_and_translate(source) + service = schema.services[0] + + # Client streaming + m1 = service.methods[0] + assert m1.name == "ClientStream" + assert m1.client_streaming is True + assert m1.server_streaming is False + + # Server streaming + m2 = service.methods[1] + assert m2.name == "ServerStream" + assert m2.client_streaming is False + assert m2.server_streaming is True + + # Bidi streaming + m3 = service.methods[2] + assert m3.name == "BidiStream" + assert m3.client_streaming is True + assert m3.server_streaming is True + + +def test_service_options(): + source = """ + syntax = "proto3"; + package demo; + + service OptionsService { + option deprecated = true; + rpc Method (Req) returns (Res); + } + + message Req {} + message Res {} + """ + schema = parse_and_translate(source) + service = schema.services[0] + assert service.options["deprecated"] is True From 9c7dd5faaf8f9c99bc6d119bb64107777f5ea239 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 8 Feb 2026 19:13:49 +0530 Subject: [PATCH 17/17] ci fix --- compiler/fory_compiler/tests/test_proto_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/fory_compiler/tests/test_proto_service.py b/compiler/fory_compiler/tests/test_proto_service.py index c8bc7f91b2..c7878cbc39 100644 --- a/compiler/fory_compiler/tests/test_proto_service.py +++ b/compiler/fory_compiler/tests/test_proto_service.py @@ -84,19 +84,19 @@ def test_streaming_rpc(): """ schema = parse_and_translate(source) service = schema.services[0] - + # Client streaming m1 = service.methods[0] assert m1.name == "ClientStream" assert m1.client_streaming is True assert m1.server_streaming is False - + # Server streaming m2 = service.methods[1] assert m2.name == "ServerStream" assert m2.client_streaming is False assert m2.server_streaming is True - + # Bidi streaming m3 = service.methods[2] assert m3.name == "BidiStream"