This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 6735b3788 feat: make generate enum use id for wire format (#3576)
6735b3788 is described below
commit 6735b37889ac904ec014298ee7ef4fd470c84823
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Apr 16 14:06:05 2026 +0800
feat: make generate enum use id for wire format (#3576)
## Why?
## What does this PR do?
make generate enum use id for wire format
- java
- python
- c++
- swift
- javascript
## 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
---
.github/workflows/release-javascript.yaml | 140 +++++++++++++++++++++
ci/release.py | 2 +-
compiler/fory_compiler/generators/java.py | 48 ++++---
.../fory_compiler/tests/test_generated_code.py | 26 ++++
.../fory_compiler/tests/test_package_options.py | 12 ++
cpp/fory/serialization/enum_serializer.h | 62 ++++++++-
cpp/fory/serialization/serialization_test.cc | 31 +++++
integration_tests/idl_tests/idl/auto_id.fdl | 4 +-
.../idl_tests/javascript/test/roundtrip.test.ts | 4 +-
javascript/packages/core/lib/gen/enum.ts | 47 +++++--
javascript/test/enum.test.ts | 13 +-
python/pyfory/_serializer.py | 28 ++++-
python/pyfory/serialization.pyx | 40 +++++-
python/pyfory/tests/test_serializer.py | 14 ++-
swift/Sources/ForyMacro/ForyObjectMacro.swift | 47 ++++++-
swift/Tests/ForyTests/ForySwiftTests.swift | 23 ++++
16 files changed, 496 insertions(+), 45 deletions(-)
diff --git a/.github/workflows/release-javascript.yaml
b/.github/workflows/release-javascript.yaml
new file mode 100644
index 000000000..359c1649e
--- /dev/null
+++ b/.github/workflows/release-javascript.yaml
@@ -0,0 +1,140 @@
+# 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: Publish JavaScript
+run-name: "JavaScript Release: ${{ github.ref_name }}"
+
+on:
+ push:
+ tags: ['v*']
+
+permissions:
+ contents: read
+ id-token: write
+
+concurrency:
+ group: release-javascript-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ publish-npm:
+ runs-on: ubuntu-latest
+ if: github.repository == 'apache/fory'
+ steps:
+ - uses: actions/checkout@v5
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Upgrade npm for trusted publishing
+ run: npm i -g npm@latest
+
+ - name: Verify Node/npm versions
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "Node: $(node --version)"
+ echo "npm: $(npm --version)"
+ REQUIRED="11.5.1"
+ CURRENT="$(npm --version)"
+ if [[ "$(printf '%s\n' "$REQUIRED" "$CURRENT" | sort -V | head -n1)"
!= "$REQUIRED" ]]; then
+ echo "npm version must be >= $REQUIRED for trusted publishing.
current=$CURRENT"
+ exit 1
+ fi
+
+ - name: Bump javascript version
+ shell: bash
+ run: |
+ set -euo pipefail
+ VERSION="${{ github.ref_name }}"
+ VERSION="${VERSION#v}"
+ python ci/release.py bump_version -l javascript -version "$VERSION"
+
+ - name: Install dependencies
+ shell: bash
+ working-directory: javascript
+ run: npm install
+
+ - name: Build packages
+ shell: bash
+ working-directory: javascript
+ run: npm run build
+
+ - name: Prepare npm for trusted publishing
+ shell: bash
+ run: |
+ set -euo pipefail
+ npm config set registry https://registry.npmjs.org/
+ npm config delete //registry.npmjs.org/:_authToken || true
+ npm config get registry
+
+ - name: Verify repository metadata for provenance
+ shell: bash
+ run: |
+ set -euo pipefail
+ node <<'EOF'
+ const fs = require("node:fs");
+
+ const expected = "[email protected]:apache/fory.git";
+ const files = [
+ "javascript/packages/core/package.json",
+ "javascript/packages/hps/package.json"
+ ];
+
+ for (const file of files) {
+ const json = JSON.parse(fs.readFileSync(file, "utf8"));
+ const actual = json?.repository ?? "";
+ if (actual !== expected) {
+ console.error(
+ `Repository URL mismatch in ${file}: expected "${expected}",
got "${actual}"`
+ );
+ process.exit(1);
+ }
+ }
+
+ console.log("repository metadata verified for all publishable
packages");
+ EOF
+
+ - name: Publish @apache-fory/hps
+ shell: bash
+ working-directory: javascript/packages/hps
+ run: |
+ set -euo pipefail
+ TAG="latest"
+ if echo "${{ github.ref_name }}" | grep -qE '[-]'; then
+ TAG="next"
+ fi
+ npm publish --access public --tag "$TAG" --provenance
+ env:
+ NODE_AUTH_TOKEN: ""
+ NPM_TOKEN: ""
+
+ - name: Publish @apache-fory/core
+ shell: bash
+ working-directory: javascript/packages/core
+ run: |
+ set -euo pipefail
+ TAG="latest"
+ if echo "${{ github.ref_name }}" | grep -qE '[-]'; then
+ TAG="next"
+ fi
+ npm publish --access public --tag "$TAG" --provenance
+ env:
+ NODE_AUTH_TOKEN: ""
+ NPM_TOKEN: ""
diff --git a/ci/release.py b/ci/release.py
index 0caf8aaa0..b919b3344 100644
--- a/ci/release.py
+++ b/ci/release.py
@@ -177,7 +177,7 @@ def bump_version(**kwargs):
bump_python_version(new_version)
elif lang == "javascript":
_bump_version(
- "javascript/packages/fory",
+ "javascript/packages/core",
"package.json",
_normalize_js_version(new_version),
_update_js_version,
diff --git a/compiler/fory_compiler/generators/java.py
b/compiler/fory_compiler/generators/java.py
index 8de256494..ee1d741bd 100644
--- a/compiler/fory_compiler/generators/java.py
+++ b/compiler/fory_compiler/generators/java.py
@@ -366,18 +366,14 @@ class JavaGenerator(BaseGenerator):
lines.append(f"package {java_package};")
lines.append("")
- # Enum declaration
- lines.append(f"public enum {enum.name} {{")
-
- # Enum values (strip prefix for scoped enums)
- for i, value in enumerate(enum.values):
- comma = "," if i < len(enum.values) - 1 else ";"
- stripped_name = self.strip_enum_prefix(enum.name, value.name)
- lines.append(f" {stripped_name}{comma}")
-
- lines.append("}")
+ lines.append("import org.apache.fory.annotation.ForyEnumId;")
lines.append("")
+ # Enum declaration
+ lines.extend(
+ self.generate_enum_declaration(enum, f"public enum {enum.name} {{")
+ )
+
# Build file path
path = self.get_java_package_path()
if path:
@@ -531,7 +527,7 @@ class JavaGenerator(BaseGenerator):
for message in self.schema.messages:
self.collect_message_imports(message, imports)
for enum in self.schema.enums:
- pass # Enums don't need special imports
+ self.collect_enum_imports(imports)
for union in self.schema.unions:
self.collect_union_imports(union, imports)
@@ -604,12 +600,19 @@ class JavaGenerator(BaseGenerator):
if self.has_array_field_recursive(message):
imports.add("java.util.Arrays")
+ for nested_enum in message.nested_enums:
+ self.collect_enum_imports(imports)
+
# Collect imports from nested messages
for nested_msg in message.nested_messages:
self.collect_message_imports(nested_msg, imports)
for nested_union in message.nested_unions:
self.collect_union_imports(nested_union, imports)
+ def collect_enum_imports(self, imports: Set[str]):
+ """Collect imports required by generated Java enums."""
+ imports.add("org.apache.fory.annotation.ForyEnumId")
+
def collect_union_imports(self, union: Union, imports: Set[str]):
"""Collect imports for a union and its cases."""
imports.add("org.apache.fory.type.union.Union")
@@ -635,15 +638,30 @@ class JavaGenerator(BaseGenerator):
def generate_nested_enum(self, enum: Enum) -> List[str]:
"""Generate a nested enum as a static inner class."""
- lines = []
- lines.append(f"public static enum {enum.name} {{")
+ return self.generate_enum_declaration(
+ enum, f"public static enum {enum.name} {{"
+ )
+
+ def generate_enum_declaration(self, enum: Enum, header: str) -> List[str]:
+ """Generate a Java enum declaration with stable Fory enum ids."""
+ lines = [header]
- # Enum values (strip prefix for scoped enums)
for i, value in enumerate(enum.values):
comma = "," if i < len(enum.values) - 1 else ";"
stripped_name = self.strip_enum_prefix(enum.name, value.name)
- lines.append(f" {stripped_name}{comma}")
+ lines.append(f" {stripped_name}({value.value}){comma}")
+ lines.append("")
+ lines.append(" private final int id;")
+ lines.append("")
+ lines.append(f" {enum.name}(int id) {{")
+ lines.append(" this.id = id;")
+ lines.append(" }")
+ lines.append("")
+ lines.append(" @ForyEnumId")
+ lines.append(" public int getId() {")
+ lines.append(" return id;")
+ lines.append(" }")
lines.append("}")
lines.append("")
return lines
diff --git a/compiler/fory_compiler/tests/test_generated_code.py
b/compiler/fory_compiler/tests/test_generated_code.py
index 34790883a..4385e07ed 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -543,6 +543,32 @@ def
test_java_repeated_float16_generation_uses_float16_list():
assert "private Float16List vals;" in java_output
+def test_java_enum_generation_uses_fory_enum_ids():
+ schema = parse_fdl(
+ dedent(
+ """
+ package gen;
+
+ enum Status {
+ UNKNOWN = 4096;
+ OK = 8192;
+ }
+ """
+ )
+ )
+ java_output = render_files(generate_files(schema, JavaGenerator))
+ assert "import org.apache.fory.annotation.ForyEnumId;" in java_output
+ assert "public enum Status {" in java_output
+ assert "UNKNOWN(4096)," in java_output
+ assert "OK(8192);" in java_output
+ assert "private final int id;" in java_output
+ assert "Status(int id) {" in java_output
+ assert "this.id = id;" in java_output
+ assert "@ForyEnumId" in java_output
+ assert "public int getId() {" in java_output
+ assert "return id;" in java_output
+
+
def test_go_bfloat16_generation():
idl = dedent(
"""
diff --git a/compiler/fory_compiler/tests/test_package_options.py
b/compiler/fory_compiler/tests/test_package_options.py
index 89a09f35a..3bbf20d58 100644
--- a/compiler/fory_compiler/tests/test_package_options.py
+++ b/compiler/fory_compiler/tests/test_package_options.py
@@ -494,9 +494,15 @@ class TestJavaOuterClassname:
assert "myapp/DescriptorProtos.java" == outer_file.path
# Check content
+ assert "import org.apache.fory.annotation.ForyEnumId;" in
outer_file.content
assert "public final class DescriptorProtos" in outer_file.content
assert "private DescriptorProtos()" in outer_file.content
assert "public static enum Status" in outer_file.content
+ assert "UNKNOWN(0)," in outer_file.content
+ assert "ACTIVE(1);" in outer_file.content
+ assert "private final int id;" in outer_file.content
+ assert "@ForyEnumId" in outer_file.content
+ assert "public int getId()" in outer_file.content
assert "public static class User" in outer_file.content
assert "public static class Order" in outer_file.content
@@ -606,6 +612,12 @@ class TestJavaOuterClassname:
assert "Status.java" in file_names
assert "User.java" in file_names
assert "MyappForyRegistration.java" in file_names
+ status_file = next(f for f in files if f.path.endswith("Status.java"))
+ assert "import org.apache.fory.annotation.ForyEnumId;" in
status_file.content
+ assert "UNKNOWN(0);" in status_file.content
+ assert "private final int id;" in status_file.content
+ assert "@ForyEnumId" in status_file.content
+ assert "public int getId()" in status_file.content
class TestJavaMultipleFiles:
diff --git a/cpp/fory/serialization/enum_serializer.h
b/cpp/fory/serialization/enum_serializer.h
index ce125c412..f931764b8 100644
--- a/cpp/fory/serialization/enum_serializer.h
+++ b/cpp/fory/serialization/enum_serializer.h
@@ -37,14 +37,35 @@ namespace serialization {
/// Serializer specialization for enum types.
///
-/// Writes the enum ordinal (underlying integral value) to match the xlang
-/// specification for value-based enums.
+/// Writes enum values using xlang-compatible uint32 wire ids. Registered enums
+/// with contiguous zero-based values keep ordinal encoding; enums with sparse
+/// non-negative declared values preserve those declared values on the wire.
template <typename E>
struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
static constexpr TypeId type_id = TypeId::ENUM;
using Metadata = meta::EnumMetadata<E>;
using OrdinalType = typename Metadata::OrdinalType;
+ using EnumInfo = meta::EnumInfo<E>;
+
+ static constexpr bool supports_explicit_wire_values() {
+ if constexpr (!EnumInfo::defined) {
+ return true;
+ }
+ for (std::size_t i = 0; i < EnumInfo::size; ++i) {
+ const auto raw = static_cast<OrdinalType>(EnumInfo::values[i]);
+ if constexpr (std::is_signed_v<OrdinalType>) {
+ if (raw < 0) {
+ return false;
+ }
+ }
+ using Unsigned = std::make_unsigned_t<OrdinalType>;
+ if (static_cast<Unsigned>(raw) != static_cast<Unsigned>(i)) {
+ return true;
+ }
+ }
+ return false;
+ }
static inline void write_type_info(WriteContext &ctx) {
// Use compile-time type lookup for faster enum type info writing
@@ -72,6 +93,24 @@ struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
}
static inline void write_data(E value, WriteContext &ctx) {
+ if constexpr (supports_explicit_wire_values()) {
+ if constexpr (EnumInfo::defined) {
+ if (!EnumInfo::contains(value)) {
+ ctx.set_error(Error::unknown_enum("Unknown enum value"));
+ return;
+ }
+ }
+ const auto raw_value = static_cast<OrdinalType>(value);
+ if constexpr (std::is_signed_v<OrdinalType>) {
+ if (raw_value < 0) {
+ ctx.set_error(
+ Error::unknown_enum("Negative enum values are not supported"));
+ return;
+ }
+ }
+ ctx.write_var_uint32(static_cast<uint32_t>(raw_value));
+ return;
+ }
OrdinalType ordinal{};
if (!Metadata::to_ordinal(value, &ordinal)) {
ctx.set_error(Error::unknown_enum("Unknown enum value"));
@@ -111,6 +150,25 @@ struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return E{};
}
+ if constexpr (supports_explicit_wire_values()) {
+ if (raw_ordinal >
+ static_cast<uint32_t>(std::numeric_limits<OrdinalType>::max())) {
+ ctx.set_error(Error::unknown_enum(
+ "Invalid enum value: " +
+ std::to_string(static_cast<unsigned long long>(raw_ordinal))));
+ return E{};
+ }
+ const auto value = static_cast<E>(static_cast<OrdinalType>(raw_ordinal));
+ if constexpr (EnumInfo::defined) {
+ if (!EnumInfo::contains(value)) {
+ ctx.set_error(Error::unknown_enum(
+ "Invalid enum value: " +
+ std::to_string(static_cast<unsigned long long>(raw_ordinal))));
+ return E{};
+ }
+ }
+ return value;
+ }
OrdinalType ordinal = static_cast<OrdinalType>(raw_ordinal);
E value{};
if (!Metadata::from_ordinal(ordinal, &value)) {
diff --git a/cpp/fory/serialization/serialization_test.cc
b/cpp/fory/serialization/serialization_test.cc
index 2f6b8ab63..d5ec5a86e 100644
--- a/cpp/fory/serialization/serialization_test.cc
+++ b/cpp/fory/serialization/serialization_test.cc
@@ -68,6 +68,8 @@ struct NestedStruct {
enum class Color { RED, GREEN, BLUE };
enum class LegacyStatus : int32_t { NEG = -3, ZERO = 0, LARGE = 42 };
FORY_ENUM(LegacyStatus, NEG, ZERO, LARGE);
+enum class SparseStatus : int32_t { UNKNOWN = 4096, OK = 8192 };
+FORY_ENUM(SparseStatus, UNKNOWN, OK);
enum OldStatus : int32_t { OLD_NEG = -7, OLD_ZERO = 0, OLD_POS = 13 };
FORY_ENUM(::OldStatus, OLD_NEG, OLD_ZERO, OLD_POS);
@@ -92,6 +94,7 @@ inline void register_test_types(Fory &fory) {
// Register all enum types used in tests
fory.register_enum<Color>(type_id++);
fory.register_enum<LegacyStatus>(type_id++);
+ fory.register_enum<SparseStatus>(type_id++);
fory.register_enum<OldStatus>(type_id++);
}
@@ -225,6 +228,11 @@ TEST(SerializationTest, OldEnumRoundtrip) {
test_roundtrip(OldStatus::OLD_POS);
}
+TEST(SerializationTest, SparseEnumRoundtrip) {
+ test_roundtrip(SparseStatus::UNKNOWN);
+ test_roundtrip(SparseStatus::OK);
+}
+
TEST(SerializationTest, EnumSerializesOrdinalValue) {
auto fory = Fory::builder().xlang(true).track_ref(false).build();
fory.register_enum<LegacyStatus>(1);
@@ -308,6 +316,29 @@ TEST(SerializationTest,
EnumOrdinalMappingRejectsInvalidOrdinal) {
EXPECT_FALSE(decode.ok());
}
+TEST(SerializationTest, SparseEnumSerializesExplicitValue) {
+ auto fory = Fory::builder().xlang(true).track_ref(false).build();
+ fory.register_enum<SparseStatus>(1);
+
+ auto bytes_result = fory.serialize(SparseStatus::OK);
+ ASSERT_TRUE(bytes_result.ok())
+ << "Serialization failed: " << bytes_result.error().to_string();
+
+ std::vector<uint8_t> bytes = bytes_result.value();
+ ASSERT_GE(bytes.size(), 1 + 1 + 1 + 1 + 2);
+ size_t offset = 1;
+ EXPECT_EQ(bytes[offset], static_cast<uint8_t>(NOT_NULL_VALUE_FLAG));
+ EXPECT_EQ(bytes[offset + 1], static_cast<uint8_t>(TypeId::ENUM));
+ EXPECT_EQ(bytes[offset + 2], 1);
+ EXPECT_EQ(bytes[offset + 3], 0x80);
+ EXPECT_EQ(bytes[offset + 4], 0x40);
+
+ auto roundtrip = fory.deserialize<SparseStatus>(bytes.data(), bytes.size());
+ ASSERT_TRUE(roundtrip.ok())
+ << "Deserialization failed: " << roundtrip.error().to_string();
+ EXPECT_EQ(roundtrip.value(), SparseStatus::OK);
+}
+
TEST(SerializationTest, OldEnumOrdinalMappingHandlesNonZeroStart) {
auto fory = Fory::builder().xlang(true).track_ref(false).build();
fory.register_enum<OldStatus>(1);
diff --git a/integration_tests/idl_tests/idl/auto_id.fdl
b/integration_tests/idl_tests/idl/auto_id.fdl
index f5a8a83ac..90946605d 100644
--- a/integration_tests/idl_tests/idl/auto_id.fdl
+++ b/integration_tests/idl_tests/idl/auto_id.fdl
@@ -18,8 +18,8 @@
package auto_id;
enum Status {
- UNKNOWN = 0;
- OK = 1;
+ UNKNOWN = 4096;
+ OK = 8192;
}
message Envelope {
diff --git a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
index 0b8ba58d0..f21535024 100644
--- a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
+++ b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
@@ -182,8 +182,8 @@ describe('Generated types compile and construct correctly',
() => {
expect(myEnvelope.payload?.value).toBe(42);
expect(myEnvelope.status).toBe(Status.OK);
- expect(Status.UNKNOWN).toBe(0);
- expect(Status.OK).toBe(1);
+ expect(Status.UNKNOWN).toBe(4096);
+ expect(Status.OK).toBe(8192);
expect(WrapperCase.ENVELOPE).toBe(1);
expect(WrapperCase.RAW).toBe(2);
diff --git a/javascript/packages/core/lib/gen/enum.ts
b/javascript/packages/core/lib/gen/enum.ts
index 20a9d91f2..22c70208b 100644
--- a/javascript/packages/core/lib/gen/enum.ts
+++ b/javascript/packages/core/lib/gen/enum.ts
@@ -32,15 +32,44 @@ class EnumSerializerGenerator extends
BaseSerializerGenerator {
this.typeInfo = typeInfo;
}
+ private getEnumEntries(): Array<[string, string | number]> {
+ const enumProps = this.typeInfo.options?.enumProps;
+ if (!enumProps) {
+ return [];
+ }
+ return Object.entries(enumProps).filter(([key, value]) => {
+ return !(typeof value === "string" && Number.isInteger(Number(key)));
+ });
+ }
+
+ private useExplicitNumericWireValues(entries: Array<[string, string |
number]>): boolean {
+ if (entries.length < 1) {
+ throw new Error("An enum must contain at least one field");
+ }
+ const seen = new Set<number>();
+ for (const [, value] of entries) {
+ if (typeof value === "string") {
+ return false;
+ }
+ if (!Number.isInteger(value) || value > MaxUInt32 || value < 0) {
+ throw new Error("Enum value must be a valid uint32");
+ }
+ if (seen.has(value)) {
+ throw new Error("Enum numeric values must be unique");
+ }
+ seen.add(value);
+ }
+ return true;
+ }
+
write(accessor: string): string {
if (!this.typeInfo.options?.enumProps) {
return this.builder.writer.writeVarUInt32(accessor);
}
- if (Object.values(this.typeInfo.options.enumProps).length < 1) {
- throw new Error("An enum must contain at least one field");
- }
+ const enumEntries = this.getEnumEntries();
+ const useExplicitNumericWireValues =
this.useExplicitNumericWireValues(enumEntries);
return `
- ${Object.values(this.typeInfo.options.enumProps).map((value, index) =>
{
+ ${enumEntries.map(([, value], index) => {
if (typeof value !== "string" && typeof value !== "number") {
throw new Error("Enum value must be string or number");
}
@@ -50,8 +79,9 @@ class EnumSerializerGenerator extends BaseSerializerGenerator
{
}
}
const safeValue = typeof value === "string" ? `"${value}"` : value;
+ const wireValue = useExplicitNumericWireValues ? safeValue : index;
return ` if (${accessor} === ${safeValue}) {
- ${this.builder.writer.writeVarUInt32(index)}
+ ${this.builder.writer.writeVarUInt32(wireValue)}
}`;
}).join(" else ")}
else {
@@ -127,11 +157,13 @@ class EnumSerializerGenerator extends
BaseSerializerGenerator {
if (!this.typeInfo.options?.enumProps) {
return accessor(this.builder.reader.readVarUInt32());
}
+ const enumEntries = this.getEnumEntries();
+ const useExplicitNumericWireValues =
this.useExplicitNumericWireValues(enumEntries);
const enumValue = this.scope.uniqueName("enum_v");
return `
const ${enumValue} = ${this.builder.reader.readVarUInt32()};
switch(${enumValue}) {
- ${Object.values(this.typeInfo.options.enumProps).map((value,
index) => {
+ ${enumEntries.map(([, value], index) => {
if (typeof value !== "string" && typeof value !== "number") {
throw new Error("Enum value must be string or number");
}
@@ -141,8 +173,9 @@ class EnumSerializerGenerator extends
BaseSerializerGenerator {
}
}
const safeValue = typeof value === "string" ? `"${value}"` : `${value}`;
+ const wireValue = useExplicitNumericWireValues ? safeValue : `${index}`;
return `
- case ${index}:
+ case ${wireValue}:
${accessor(safeValue)}
break;
`;
diff --git a/javascript/test/enum.test.ts b/javascript/test/enum.test.ts
index 4d5f72cb4..26aa5407d 100644
--- a/javascript/test/enum.test.ts
+++ b/javascript/test/enum.test.ts
@@ -62,6 +62,18 @@ describe('enum', () => {
expect(result).toEqual(Foo.f1)
});
+ test('should preserve sparse numeric enum values', () => {
+ const Foo = {
+ unknown: 4096,
+ ok: 8192
+ };
+ const fory = new Fory({ ref: true });
+ const { serialize, deserialize } = fory.register(Type.enum("example.foo",
Foo));
+ const input = serialize(Foo.ok);
+ const result = deserialize(input);
+ expect(result).toEqual(Foo.ok);
+ });
+
test('should typescript string enum work', () => {
enum Foo {
f1 = "hello",
@@ -76,4 +88,3 @@ describe('enum', () => {
expect(result).toEqual(Foo.f1)
});
});
-
diff --git a/python/pyfory/_serializer.py b/python/pyfory/_serializer.py
index 7e46a653b..6b547cece 100644
--- a/python/pyfory/_serializer.py
+++ b/python/pyfory/_serializer.py
@@ -301,18 +301,38 @@ class EnumSerializer(Serializer):
super().__init__(type_resolver, type_)
self.need_to_write_ref = False
self._members = tuple(type_)
- self._ordinal_by_member = {member: idx for idx, member in
enumerate(self._members)}
+ self._wire_value_by_member = {member: idx for idx, member in
enumerate(self._members)}
+ self._member_by_wire_value = {idx: member for idx, member in
enumerate(self._members)}
+ if type_resolver.xlang:
+ explicit_wire_values = {}
+ use_explicit_ids = True
+ for member in self._members:
+ raw_value = member.value
+ if isinstance(raw_value, bool) or not isinstance(raw_value,
int) or raw_value < 0:
+ use_explicit_ids = False
+ break
+ wire_value = int(raw_value)
+ if wire_value in explicit_wire_values:
+ use_explicit_ids = False
+ break
+ explicit_wire_values[wire_value] = member
+ if use_explicit_ids:
+ self._wire_value_by_member = {member: int(member.value) for
member in self._members}
+ self._member_by_wire_value = explicit_wire_values
@classmethod
def support_subclass(cls) -> bool:
return True
def write(self, write_context, value):
- write_context.write_var_uint32(self._ordinal_by_member[value])
+ write_context.write_var_uint32(self._wire_value_by_member[value])
def read(self, read_context):
- ordinal = read_context.read_var_uint32()
- return self._members[ordinal]
+ wire_value = read_context.read_var_uint32()
+ try:
+ return self._member_by_wire_value[wire_value]
+ except KeyError as exc:
+ raise ValueError(f"Unknown enum value {wire_value} for
{self.type_.__qualname__}") from exc
class SliceSerializer(Serializer):
diff --git a/python/pyfory/serialization.pyx b/python/pyfory/serialization.pyx
index e119d9fd8..0ccea8963 100644
--- a/python/pyfory/serialization.pyx
+++ b/python/pyfory/serialization.pyx
@@ -608,24 +608,54 @@ cdef class Serializer:
@cython.final
cdef class EnumSerializer(Serializer):
cdef tuple _members
- cdef dict _ordinal_by_member
+ cdef dict _wire_value_by_member
+ cdef dict _member_by_wire_value
def __init__(self, TypeResolver type_resolver, type_):
+ cdef dict explicit_wire_values
+ cdef bint use_explicit_ids
+ cdef object member
+ cdef object raw_value
+ cdef uint32_t wire_value
super().__init__(type_resolver, type_)
self.need_to_write_ref = False
self._members = tuple(type_)
- self._ordinal_by_member = {member: idx for idx, member in
enumerate(self._members)}
+ self._wire_value_by_member = {member: idx for idx, member in
enumerate(self._members)}
+ self._member_by_wire_value = {idx: member for idx, member in
enumerate(self._members)}
+ if type_resolver.xlang:
+ explicit_wire_values = {}
+ use_explicit_ids = True
+ for member in self._members:
+ raw_value = member.value
+ if isinstance(raw_value, bool) or not isinstance(raw_value,
int) or raw_value < 0:
+ use_explicit_ids = False
+ break
+ wire_value = raw_value
+ if wire_value in explicit_wire_values:
+ use_explicit_ids = False
+ break
+ explicit_wire_values[wire_value] = member
+ if use_explicit_ids:
+ self._wire_value_by_member = {
+ member: int(member.value) for member in self._members
+ }
+ self._member_by_wire_value = explicit_wire_values
@classmethod
def support_subclass(cls) -> bool:
return True
cpdef inline write(self, WriteContext write_context, value):
- write_context.write_var_uint32(self._ordinal_by_member[value])
+ write_context.write_var_uint32(self._wire_value_by_member[value])
cpdef inline read(self, ReadContext read_context):
- cdef uint32_t ordinal = read_context.read_var_uint32()
- return self._members[ordinal]
+ cdef uint32_t wire_value = read_context.read_var_uint32()
+ cdef object value = self._member_by_wire_value.get(wire_value)
+ if value is None:
+ raise ValueError(
+ f"Unknown enum value {wire_value} for
{self.type_.__qualname__}"
+ )
+ return value
@cython.final
diff --git a/python/pyfory/tests/test_serializer.py
b/python/pyfory/tests/test_serializer.py
index 798772cb5..8fc963b8a 100644
--- a/python/pyfory/tests/test_serializer.py
+++ b/python/pyfory/tests/test_serializer.py
@@ -22,7 +22,7 @@ import io
import os
import pickle
import weakref
-from enum import Enum
+from enum import Enum, IntEnum
from typing import Any, List, Dict, Optional
import numpy as np
@@ -503,6 +503,11 @@ class EnumClass(Enum):
E4 = "E4"
+class SparseIntEnum(IntEnum):
+ A = 4096
+ B = 8192
+
+
def test_enum():
fory = Fory(xlang=False, ref=True)
assert ser_de(fory, EnumClass.E1) == EnumClass.E1
@@ -512,6 +517,13 @@ def test_enum():
assert isinstance(fory.type_resolver.get_serializer(EnumClass),
EnumSerializer)
+def test_xlang_enum_uses_sparse_integer_values():
+ fory = Fory(xlang=True, ref=False)
+ fory.register_type(SparseIntEnum, type_id=301)
+ assert ser_de(fory, SparseIntEnum.A) == SparseIntEnum.A
+ assert ser_de(fory, SparseIntEnum.B) == SparseIntEnum.B
+
+
def test_duplicate_serialize():
fory = Fory(xlang=False, ref=True)
assert ser_de(fory, EnumClass.E1) == EnumClass.E1
diff --git a/swift/Sources/ForyMacro/ForyObjectMacro.swift
b/swift/Sources/ForyMacro/ForyObjectMacro.swift
index 63948c23a..03c57b3e5 100644
--- a/swift/Sources/ForyMacro/ForyObjectMacro.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift
@@ -196,6 +196,7 @@ private struct ParsedEnumCase {
let name: String
let payload: [ParsedEnumPayloadField]
let caseID: Int?
+ let wireValue: UInt32?
}
private struct ParsedEnumDecl {
@@ -219,6 +220,7 @@ private struct ParsedForyObjectConfiguration {
private func parseEnumDecl(_ enumDecl: EnumDeclSyntax) throws ->
ParsedEnumDecl {
var cases: [ParsedEnumCase] = []
+ let integerRawEnum = enumDeclUsesExplicitIntegerRawValues(enumDecl)
for member in enumDecl.memberBlock.members {
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
@@ -275,7 +277,8 @@ private func parseEnumDecl(_ enumDecl: EnumDeclSyntax)
throws -> ParsedEnumDecl
.init(
name: caseName,
payload: payloadFields,
- caseID: caseConfig?.id
+ caseID: caseConfig?.id,
+ wireValue: integerRawEnum ?
parseEnumCaseWireValue(element) : nil
)
)
}
@@ -317,15 +320,19 @@ private func buildEnumDecls(_ parsedEnum: ParsedEnumDecl,
accessPrefix: String)
private func buildOrdinalEnumDecls(_ cases: [ParsedEnumCase], accessPrefix:
String) -> [DeclSyntax] {
let defaultCase = cases[0].name
+ let useExplicitWireValues = cases.allSatisfy { $0.wireValue != nil }
let writeSwitchCases = cases.enumerated().map { index, enumCase in
- """
+ let wireValue = enumCase.wireValue ?? UInt32(index)
+ return """
case .\(enumCase.name):
- context.buffer.writeVarUInt32(\(index))
+ context.buffer.writeVarUInt32(\(wireValue))
"""
}.joined(separator: "\n ")
let readSwitchCases = cases.enumerated().map { index, enumCase in
- "case \(index): return .\(enumCase.name)"
+ let wireValue = enumCase.wireValue ?? UInt32(index)
+ return "case \(wireValue): return .\(enumCase.name)"
}.joined(separator: "\n ")
+ let errorLabel = useExplicitWireValues ? "enum value" : "enum ordinal"
let defaultDecl: DeclSyntax = DeclSyntax(
stringLiteral: """
@@ -360,7 +367,7 @@ private func buildOrdinalEnumDecls(_ cases:
[ParsedEnumCase], accessPrefix: Stri
switch ordinal {
\(readSwitchCases)
default:
- throw ForyError.invalidData("unknown enum ordinal \\(ordinal)")
+ throw ForyError.invalidData("unknown \(errorLabel)
\\(ordinal)")
}
}
"""
@@ -369,6 +376,36 @@ private func buildOrdinalEnumDecls(_ cases:
[ParsedEnumCase], accessPrefix: Stri
return [defaultDecl, staticTypeIDDecl, writeWrapperDecl, writeDecl,
readDecl]
}
+private func enumDeclUsesExplicitIntegerRawValues(_ enumDecl: EnumDeclSyntax)
-> Bool {
+ guard let inheritanceClause = enumDecl.inheritanceClause else {
+ return false
+ }
+ let inheritedTypes = inheritanceClause.inheritedTypes.map {
$0.type.trimmedDescription }
+ return inheritedTypes.contains {
+ [
+ "Int",
+ "Int8",
+ "Int16",
+ "Int32",
+ "Int64",
+ "UInt",
+ "UInt8",
+ "UInt16",
+ "UInt32",
+ "UInt64",
+ ].contains($0)
+ }
+}
+
+private func parseEnumCaseWireValue(_ element: EnumCaseElementSyntax) ->
UInt32? {
+ guard let rawValue = element.rawValue?.value.trimmedDescription,
+ let parsed = UInt32(rawValue)
+ else {
+ return nil
+ }
+ return parsed
+}
+
private func buildTaggedUnionEnumDecls(_ cases: [ParsedEnumCase],
accessPrefix: String) -> [DeclSyntax] {
let defaultExpr = enumCaseDefaultExpr(cases[0])
let writeSwitchCases = cases.enumerated().map { index, enumCase in
diff --git a/swift/Tests/ForyTests/ForySwiftTests.swift
b/swift/Tests/ForyTests/ForySwiftTests.swift
index e7e347311..146138d86 100644
--- a/swift/Tests/ForyTests/ForySwiftTests.swift
+++ b/swift/Tests/ForyTests/ForySwiftTests.swift
@@ -80,6 +80,12 @@ struct FieldIdTarget: Equatable {
var renamedLabel: String
}
+@ForyObject
+enum SparseStatus: Int32, CaseIterable {
+ case unknown = 4096
+ case ok = 8192
+}
+
@ForyObject
struct EvolvingOverrideValue: Equatable {
var f1: String = ""
@@ -724,6 +730,23 @@ func macroFieldEncodingOverridesForUnsignedTypes() throws {
#expect(try buffer.readTaggedUInt64() == value.u64Tagged)
}
+@Test
+func macroEnumUsesExplicitIntegerRawValue() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false))
+ fory.register(SparseStatus.self, id: 302)
+
+ let data = try fory.serialize(SparseStatus.ok)
+ let buffer = ByteBuffer(data: data)
+ _ = try fory.readHead(buffer: buffer)
+ _ = try buffer.readInt8()
+ _ = try buffer.readVarUInt32()
+ _ = try buffer.readVarUInt32()
+ #expect(try buffer.readVarUInt32() == 8192)
+
+ let decoded: SparseStatus = try fory.deserialize(data)
+ #expect(decoded == .ok)
+}
+
@Test
func macroFieldEncodingOverridesCompatibleTypeMeta() throws {
let fields = EncodedNumberFields.foryFieldsInfo(trackRef: false)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]