Skip to content

Commit 62283f8

Browse files
authored
feat(xlang): add decimal and align serializers for xlang (#3599)
## Why? ## What does this PR do? ## Related issues ## AI Contribution Checklist - [ ] Substantial AI assistance was used in this PR: `yes` / `no` - [ ] If `yes`, I included a completed [AI Contribution Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs) in this PR description and the required `AI Usage Disclosure`. - [ ] If `yes`, my PR description includes the required `ai_review` summary and screenshot evidence of the final clean AI review results from both fresh reviewers on the current PR diff or current HEAD after the latest code changes. ## Does this PR introduce any user-facing change? - [ ] Does this PR introduce any public API change? - [ ] Does this PR introduce any binary protocol compatibility change? ## Benchmark
1 parent 1de178f commit 62283f8

File tree

181 files changed

+6293
-1894
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

181 files changed

+6293
-1894
lines changed

compiler/fory_compiler/generators/cpp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class CppGenerator(BaseGenerator):
6666
PrimitiveKind.FLOAT64: "double",
6767
PrimitiveKind.STRING: "std::string",
6868
PrimitiveKind.BYTES: "std::vector<uint8_t>",
69+
PrimitiveKind.DECIMAL: "fory::serialization::Decimal",
6970
PrimitiveKind.DATE: "fory::serialization::Date",
7071
PrimitiveKind.TIMESTAMP: "fory::serialization::Timestamp",
7172
PrimitiveKind.ANY: "std::any",
@@ -1779,6 +1780,8 @@ def collect_includes(
17791780
includes.add("<string>")
17801781
elif field_type.kind == PrimitiveKind.BYTES:
17811782
includes.add("<vector>")
1783+
elif field_type.kind == PrimitiveKind.DECIMAL:
1784+
includes.add('"fory/serialization/decimal_serializers.h"')
17821785
elif field_type.kind in (PrimitiveKind.DATE, PrimitiveKind.TIMESTAMP):
17831786
includes.add('"fory/serialization/temporal_serializers.h"')
17841787
elif field_type.kind == PrimitiveKind.ANY:

compiler/fory_compiler/generators/dart.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class DartGenerator(BaseGenerator):
6767
PrimitiveKind.BYTES: "Uint8List",
6868
PrimitiveKind.DATE: "LocalDate",
6969
PrimitiveKind.TIMESTAMP: "Timestamp",
70+
PrimitiveKind.DECIMAL: "Decimal",
7071
PrimitiveKind.ANY: "Object?",
7172
}
7273

@@ -610,6 +611,7 @@ def _default_value_for_type(
610611
PrimitiveKind.BYTES: "Uint8List(0)",
611612
PrimitiveKind.DATE: "const LocalDate(1970, 1, 1)",
612613
PrimitiveKind.TIMESTAMP: "Timestamp(0, 0)",
614+
PrimitiveKind.DECIMAL: "const Decimal.zero()",
613615
PrimitiveKind.ANY: "null",
614616
}[t.kind]
615617
if isinstance(t, ListType):

compiler/fory_compiler/generators/go.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def message_has_unions(self, message: Message) -> bool:
197197
PrimitiveKind.BYTES: "[]byte",
198198
PrimitiveKind.DATE: "fory.Date",
199199
PrimitiveKind.TIMESTAMP: "time.Time",
200+
PrimitiveKind.DECIMAL: "fory.Decimal",
200201
PrimitiveKind.ANY: "any",
201202
}
202203

@@ -666,6 +667,7 @@ def get_union_case_type_id_expr(
666667
PrimitiveKind.BYTES: "fory.BINARY",
667668
PrimitiveKind.DATE: "fory.DATE",
668669
PrimitiveKind.TIMESTAMP: "fory.TIMESTAMP",
670+
PrimitiveKind.DECIMAL: "fory.DECIMAL",
669671
PrimitiveKind.ANY: "fory.UNKNOWN",
670672
}
671673
return primitive_type_ids.get(kind, "fory.UNKNOWN")

compiler/fory_compiler/generators/javascript.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class JavaScriptGenerator(BaseGenerator):
147147
PrimitiveKind.DATE: "Date",
148148
PrimitiveKind.TIMESTAMP: "Date",
149149
PrimitiveKind.DURATION: "number",
150-
# DECIMAL is not supported by the JS runtime; rejected in _field_type_expr.
150+
PrimitiveKind.DECIMAL: "Decimal",
151151
PrimitiveKind.ANY: "any",
152152
}
153153

@@ -177,7 +177,7 @@ class JavaScriptGenerator(BaseGenerator):
177177
PrimitiveKind.DATE: "Type.date()",
178178
PrimitiveKind.TIMESTAMP: "Type.timestamp()",
179179
PrimitiveKind.DURATION: "Type.duration()",
180-
# DECIMAL is not yet supported by the JS runtime; omitted intentionally.
180+
PrimitiveKind.DECIMAL: "Type.decimal()",
181181
PrimitiveKind.ANY: "Type.any()",
182182
}
183183

@@ -531,6 +531,9 @@ def generate_imports(self) -> List[str]:
531531
lines: List[str] = []
532532
imported_regs = self._collect_imported_registrations()
533533

534+
if self._schema_uses_primitive_kind(PrimitiveKind.DECIMAL):
535+
lines.append("import { Decimal } from '@apache-fory/core';")
536+
534537
# Collect all imported types used in this schema
535538
imported_types_by_module: Dict[str, Set[str]] = {}
536539

@@ -577,6 +580,39 @@ def generate_imports(self) -> List[str]:
577580

578581
return lines
579582

583+
def _schema_uses_primitive_kind(self, primitive_kind: PrimitiveKind) -> bool:
584+
def uses_field_type(field_type: FieldType) -> bool:
585+
if isinstance(field_type, PrimitiveType):
586+
return field_type.kind == primitive_kind
587+
if isinstance(field_type, NamedType):
588+
return field_type.name.lower() == primitive_kind.value
589+
if isinstance(field_type, ListType):
590+
return uses_field_type(field_type.element_type)
591+
if isinstance(field_type, MapType):
592+
return uses_field_type(field_type.key_type) or uses_field_type(
593+
field_type.value_type
594+
)
595+
return False
596+
597+
def uses_message(message: Message) -> bool:
598+
for field in message.fields:
599+
if uses_field_type(field.field_type):
600+
return True
601+
for nested_union in message.nested_unions:
602+
if any(
603+
uses_field_type(field.field_type) for field in nested_union.fields
604+
):
605+
return True
606+
return any(uses_message(nested) for nested in message.nested_messages)
607+
608+
if any(
609+
uses_field_type(field.field_type)
610+
for union in self.schema.unions
611+
for field in union.fields
612+
):
613+
return True
614+
return any(uses_message(message) for message in self.schema.messages)
615+
580616
def generate(self) -> List[GeneratedFile]:
581617
"""Generate JavaScript files for the schema."""
582618
return [self.generate_file()]
@@ -806,11 +842,6 @@ def _field_type_expr(
806842
"""Return the Fory JS runtime ``Type.xxx()`` expression for a field type."""
807843
parent_stack = parent_stack or []
808844
if isinstance(field_type, PrimitiveType):
809-
if field_type.kind == PrimitiveKind.DECIMAL:
810-
raise ValueError(
811-
"decimal is not supported by the JavaScript runtime. "
812-
"Use a different type or wait for runtime support."
813-
)
814845
expr = self.PRIMITIVE_RUNTIME_MAP.get(field_type.kind)
815846
if expr is None:
816847
return "Type.any()"
@@ -826,11 +857,6 @@ def _field_type_expr(
826857
return self.PRIMITIVE_RUNTIME_MAP[shorthand_map[lower]]
827858
for pk in PrimitiveKind:
828859
if pk.value == lower:
829-
if pk == PrimitiveKind.DECIMAL:
830-
raise ValueError(
831-
"decimal is not supported by the JavaScript runtime. "
832-
"Use a different type or wait for runtime support."
833-
)
834860
expr = self.PRIMITIVE_RUNTIME_MAP.get(pk)
835861
if expr is None:
836862
raise ValueError(

compiler/fory_compiler/generators/python.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class PythonGenerator(BaseGenerator):
6868
PrimitiveKind.BYTES: "bytes",
6969
PrimitiveKind.DATE: "datetime.date",
7070
PrimitiveKind.TIMESTAMP: "datetime.datetime",
71+
PrimitiveKind.DECIMAL: "decimal.Decimal",
7172
PrimitiveKind.ANY: "Any",
7273
}
7374

@@ -138,6 +139,7 @@ class PythonGenerator(BaseGenerator):
138139
PrimitiveKind.BYTES: 'b""',
139140
PrimitiveKind.DATE: "None",
140141
PrimitiveKind.TIMESTAMP: "None",
142+
PrimitiveKind.DECIMAL: 'decimal.Decimal("0")',
141143
PrimitiveKind.ANY: "None",
142144
}
143145

@@ -962,6 +964,8 @@ def collect_imports(
962964
if isinstance(field_type, PrimitiveType):
963965
if field_type.kind in (PrimitiveKind.DATE, PrimitiveKind.TIMESTAMP):
964966
imports.add("import datetime")
967+
elif field_type.kind == PrimitiveKind.DECIMAL:
968+
imports.add("import decimal")
965969
elif field_type.kind == PrimitiveKind.ANY:
966970
imports.add("from typing import Any")
967971

compiler/fory_compiler/generators/rust.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class RustGenerator(BaseGenerator):
6767
PrimitiveKind.BYTES: "Vec<u8>",
6868
PrimitiveKind.DATE: "chrono::NaiveDate",
6969
PrimitiveKind.TIMESTAMP: "chrono::NaiveDateTime",
70+
PrimitiveKind.DECIMAL: "fory::Decimal",
7071
PrimitiveKind.ANY: "Box<dyn Any>",
7172
}
7273

compiler/fory_compiler/generators/swift.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ class SwiftGenerator(BaseGenerator):
6565
PrimitiveKind.FLOAT64: "Double",
6666
PrimitiveKind.STRING: "String",
6767
PrimitiveKind.BYTES: "Data",
68-
PrimitiveKind.DATE: "ForyDate",
69-
PrimitiveKind.TIMESTAMP: "ForyTimestamp",
68+
PrimitiveKind.DATE: "LocalDate",
69+
PrimitiveKind.TIMESTAMP: "Date",
70+
PrimitiveKind.DECIMAL: "Decimal",
7071
PrimitiveKind.ANY: "Any",
7172
}
7273

@@ -963,12 +964,13 @@ def generate_message_fields(
963964

964965
encoding = self.field_encoding_argument(field)
965966
field_id = self.message_field_id_argument(field)
966-
if field_id is not None and encoding is not None:
967-
lines.append(f"{ind}@ForyField(id: {field_id}, encoding: {encoding})")
968-
elif field_id is not None:
969-
lines.append(f"{ind}@ForyField(id: {field_id})")
970-
elif encoding is not None:
971-
lines.append(f"{ind}@ForyField(encoding: {encoding})")
967+
attr_parts: List[str] = []
968+
if field_id is not None:
969+
attr_parts.append(f"id: {field_id}")
970+
if encoding is not None:
971+
attr_parts.append(f"encoding: {encoding}")
972+
if attr_parts:
973+
lines.append(f"{ind}@ForyField({', '.join(attr_parts)})")
972974

973975
field_type = self.field_swift_type(field, lineage)
974976
weak_prefix = "weak " if self.is_weak_ref_field(field) else ""

compiler/fory_compiler/tests/test_dart_generator.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ def test_dart_generator_uses_typed_lists_for_non_nullable_primitive_lists():
143143
assert "factory ValueUnion.values(Uint32List value)" in file.content
144144

145145

146+
def test_dart_generator_supports_decimal_fields_and_unions():
147+
file = generate_dart(
148+
"""
149+
package demo;
150+
151+
message Money [id=100] {
152+
decimal amount = 1;
153+
}
154+
155+
union ValueUnion [id=101] {
156+
decimal amount = 1;
157+
Money money = 2;
158+
}
159+
"""
160+
)
161+
162+
assert "Decimal amount = const Decimal.zero();" in file.content
163+
assert "factory ValueUnion.amount(Decimal value)" in file.content
164+
165+
146166
def test_dart_generator_emits_container_ref_annotations_for_builder_metadata():
147167
file = generate_dart(
148168
"""

compiler/fory_compiler/tests/test_generated_code.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,31 @@ def test_java_repeated_float16_generation_uses_float16_list():
543543
assert "private Float16List vals;" in java_output
544544

545545

546+
def test_cpp_generator_supports_decimal_fields_and_unions():
547+
schema = parse_fdl(
548+
dedent(
549+
"""
550+
package gen;
551+
552+
message Money {
553+
decimal amount = 1;
554+
}
555+
556+
union Value {
557+
decimal amount = 1;
558+
Money money = 2;
559+
}
560+
"""
561+
)
562+
)
563+
564+
cpp_output = render_files(generate_files(schema, CppGenerator))
565+
assert '#include "fory/serialization/decimal_serializers.h"' in cpp_output
566+
assert "const fory::serialization::Decimal& amount() const" in cpp_output
567+
assert "std::variant<fory::serialization::Decimal, Money> value_" in cpp_output
568+
assert "(fory::serialization::Decimal, amount, fory::F(1))" in cpp_output
569+
570+
546571
def test_java_enum_generation_uses_fory_enum_ids():
547572
schema = parse_fdl(
548573
dedent(

compiler/fory_compiler/tests/test_javascript_codegen.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,29 @@ def test_javascript_collection_types():
219219
assert "config: Map<string, number>;" in output
220220

221221

222+
def test_javascript_decimal_generation_uses_runtime_decimal_type():
223+
source = dedent(
224+
"""
225+
package example;
226+
227+
message Money [id=100] {
228+
decimal amount = 1;
229+
}
230+
231+
union Value [id=101] {
232+
decimal amount = 1;
233+
Money money = 2;
234+
}
235+
"""
236+
)
237+
output = generate_javascript(source)
238+
239+
assert "import { Decimal } from '@apache-fory/core';" in output
240+
assert "amount: Decimal;" in output
241+
assert "{ case: ValueCase.AMOUNT; value: Decimal }" in output
242+
assert "amount: Type.decimal()" in output
243+
244+
222245
def test_javascript_map_key_fallback_to_map():
223246
"""Test that map keys not valid for Record use Map<K, V> instead."""
224247
source = dedent(

0 commit comments

Comments
 (0)