diff --git a/dart/packages/fory-test/lib/entity/enum_id_foo.dart b/dart/packages/fory-test/lib/entity/enum_id_foo.dart new file mode 100644 index 0000000000..1f2f219248 --- /dev/null +++ b/dart/packages/fory-test/lib/entity/enum_id_foo.dart @@ -0,0 +1,51 @@ +/* + * 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 'package:fory/fory.dart'; + +part '../generated/enum_id_foo.g.dart'; + +@foryEnum +enum EnumWithIds { + @ForyEnumId(10) + A, + @ForyEnumId(20) + B, + @ForyEnumId(30) + C, +} + +@foryEnum +enum EnumPartialIds { + @ForyEnumId(10) + A, + B, + @ForyEnumId(30) + C, +} + +@foryEnum +enum EnumDuplicateIds { + @ForyEnumId(10) + A, + @ForyEnumId(10) + B, + @ForyEnumId(30) + C, +} diff --git a/dart/packages/fory-test/test/codegen_test/enum_codegen_test.dart b/dart/packages/fory-test/test/codegen_test/enum_codegen_test.dart index 86578caddb..f75cfc4f0a 100644 --- a/dart/packages/fory-test/test/codegen_test/enum_codegen_test.dart +++ b/dart/packages/fory-test/test/codegen_test/enum_codegen_test.dart @@ -22,6 +22,7 @@ library; import 'package:checks/checks.dart'; import 'package:fory/fory.dart'; +import 'package:fory_test/entity/enum_id_foo.dart'; import 'package:fory_test/entity/enum_foo.dart'; import 'package:test/test.dart'; @@ -31,8 +32,24 @@ void main() { EnumSpec enumSpec = EnumSpec(EnumFoo, [EnumFoo.A, EnumFoo.B]); EnumSpec enumSubTypeSpec = EnumSpec(EnumSubClass, [EnumSubClass.A, EnumSubClass.B]); + EnumSpec enumWithIdsSpec = EnumSpec( + EnumWithIds, + [EnumWithIds.A, EnumWithIds.B, EnumWithIds.C], + { + 10: EnumWithIds.A, + 20: EnumWithIds.B, + 30: EnumWithIds.C, + }); + EnumSpec enumPartialIdsSpec = + EnumSpec(EnumPartialIds,[EnumPartialIds.A, EnumPartialIds.B, EnumPartialIds.C]); + EnumSpec enumDuplicateIdsSpec = + EnumSpec(EnumDuplicateIds, [EnumDuplicateIds.A, EnumDuplicateIds.B, EnumDuplicateIds.C]); + check($EnumFoo).equals(enumSpec); check($EnumSubClass).equals(enumSubTypeSpec); + check($EnumWithIds).equals(enumWithIdsSpec); + check($EnumPartialIds).equals(enumPartialIdsSpec); + check($EnumDuplicateIds).equals(enumDuplicateIdsSpec); }); }); } diff --git a/dart/packages/fory-test/test/datatype_test/enum_serializer_test.dart b/dart/packages/fory-test/test/datatype_test/enum_serializer_test.dart new file mode 100644 index 0000000000..fbd8a6b3f5 --- /dev/null +++ b/dart/packages/fory-test/test/datatype_test/enum_serializer_test.dart @@ -0,0 +1,148 @@ +/* + * 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. + */ + +library; + +import 'package:checks/checks.dart'; +import 'package:fory/src/collection/stack.dart'; +import 'package:fory/src/config/fory_config.dart'; +import 'package:fory/src/deserialization_dispatcher.dart'; +import 'package:fory/src/deserialization_context.dart'; +import 'package:fory/src/fory_exception.dart'; +import 'package:fory/src/memory/byte_reader.dart'; +import 'package:fory/src/memory/byte_writer.dart'; +import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart'; +import 'package:fory/src/resolver/deserialization_ref_resolver.dart'; +import 'package:fory/src/resolver/meta_string_writing_resolver.dart'; +import 'package:fory/src/resolver/serialization_ref_resolver.dart'; +import 'package:fory/src/resolver/struct_hash_resolver.dart'; +import 'package:fory/src/resolver/type_resolver.dart'; +import 'package:fory/src/serialization_dispatcher.dart'; +import 'package:fory/src/serialization_context.dart'; +import 'package:fory/src/serializer/enum_serializer.dart'; +import 'package:fory_test/entity/enum_id_foo.dart'; +import 'package:test/test.dart'; + +String _unusedTagLookup(Type _) => ''; + +final ForyConfig _config = ForyConfig(); +final TypeResolver _typeResolver = TypeResolver.newOne(_config); + +SerializationContext _newSerializationContext() { + return SerializationContext( + StructHashResolver.inst, + _unusedTagLookup, + SerializationDispatcher.I, + _typeResolver, + SerializationRefResolver.getOne(false), + SerializationRefResolver.noRefResolver, + MetaStringWritingResolver.newInst, + Stack(), + ); +} + +DeserializationContext _newDeserializationContext() { + return DeserializationContext( + StructHashResolver.inst, + _unusedTagLookup, + _config, + (isXLang: true, oobEnabled: false), + DeserializationDispatcher.I, + DeserializationRefResolver.getOne(false), + _typeResolver, + Stack(), + ); +} + +void main() { + group('Enum serializer', () { + test('writes and reads annotated enum ids when all values are annotated', + () { + final EnumSerializer serializer = EnumSerializer(false, [ + EnumWithIds.A, + EnumWithIds.B, + EnumWithIds.C, + ], { + 10: EnumWithIds.A, + 20: EnumWithIds.B, + 30: EnumWithIds.C, + }); + + final ByteWriter writer = ByteWriter(); + serializer.write( + writer, + EnumWithIds.B, + _newSerializationContext(), + ); + final ByteReader encodedIdReader = + ByteReader.forBytes(writer.takeBytes()); + check(encodedIdReader.readVarUint32Small7()).equals(20); + + final ByteWriter idWriter = ByteWriter(); + idWriter.writeVarUint32Small7(30); + final Enum value = serializer.read( + ByteReader.forBytes(idWriter.takeBytes()), + 0, + _newDeserializationContext(), + ); + check(value).equals(EnumWithIds.C); + }); + + test('falls back to ordinal serialization when id mapping is absent', () { + final EnumSerializer serializer = EnumSerializer(false, [ + EnumPartialIds.A, + EnumPartialIds.B, + EnumPartialIds.C, + ]); + + final ByteWriter writer = ByteWriter(); + serializer.write( + writer, + EnumPartialIds.B, + _newSerializationContext(), + ); + final ByteReader encodedIdReader = + ByteReader.forBytes(writer.takeBytes()); + check(encodedIdReader.readVarUint32Small7()).equals(1); + }); + + test('throws on unknown annotated enum id', () { + final EnumSerializer serializer = EnumSerializer(false, [ + EnumWithIds.A, + EnumWithIds.B, + EnumWithIds.C, + ], { + 10: EnumWithIds.A, + 20: EnumWithIds.B, + 30: EnumWithIds.C, + }); + + final ByteWriter writer = ByteWriter(); + writer.writeVarUint32Small7(99); + + check( + () => serializer.read( + ByteReader.forBytes(writer.takeBytes()), + 0, + _newDeserializationContext(), + ), + ).throws(); + }); + }); +} diff --git a/dart/packages/fory/lib/src/annotation/fory_enum.dart b/dart/packages/fory/lib/src/annotation/fory_enum.dart index fe7aeab56b..264bcf5d47 100644 --- a/dart/packages/fory/lib/src/annotation/fory_enum.dart +++ b/dart/packages/fory/lib/src/annotation/fory_enum.dart @@ -31,6 +31,7 @@ import 'fory_object.dart'; /// // enums /// } /// ``` +@Target({TargetKind.enumType}) class ForyEnum extends ForyObject { static const String name = 'ForyEnum'; static const List targets = [TargetKind.enumType]; @@ -41,3 +42,24 @@ class ForyEnum extends ForyObject { /// A constant instance of [ForyEnum]. const ForyEnum foryEnum = ForyEnum(); + +/// A class representing an enumeration id in the Fory framework. +/// +/// This class extends [ForyObject] and is used to annotate enum ids +/// within the Fory framework. +/// Example: +/// ``` +/// @foryEnum +/// enum Color { +/// @ForyEnumId(5) +/// blue, +/// @ForyEnumId(10) +/// white, +// } +/// ``` +@Target({TargetKind.enumValue}) +class ForyEnumId extends ForyObject { + final int id; + + const ForyEnumId(this.id); +} \ No newline at end of file diff --git a/dart/packages/fory/lib/src/codegen/analyze/analysis_type_identifier.dart b/dart/packages/fory/lib/src/codegen/analyze/analysis_type_identifier.dart index 645f974fef..ec1671ef37 100644 --- a/dart/packages/fory/lib/src/codegen/analyze/analysis_type_identifier.dart +++ b/dart/packages/fory/lib/src/codegen/analyze/analysis_type_identifier.dart @@ -43,7 +43,8 @@ class AnalysisTypeIdentifier { null, null, null, - null + null, + null, ]; static final List _keys = [ TypeStringKey( @@ -66,6 +67,11 @@ class AnalysisTypeIdentifier { 'package', 'fory/src/annotation/fory_enum.dart', ), + TypeStringKey( + 'ForyEnumId', + 'package', + 'fory/src/annotation/fory_enum.dart', + ), TypeStringKey( 'Uint8Type', 'package', @@ -121,6 +127,10 @@ class AnalysisTypeIdentifier { return _check(element, 3); } + static bool isForyEnumId(ClassElement element) { + return _check(element, 4); + } + static void cacheForyEnumAnnotationId(int id) { _ids[3] = id; } @@ -130,18 +140,18 @@ class AnalysisTypeIdentifier { } static bool isUint8Type(ClassElement element) { - return _check(element, 4); + return _check(element, 5); } static bool isUint16Type(ClassElement element) { - return _check(element, 5); + return _check(element, 6); } static bool isUint32Type(ClassElement element) { - return _check(element, 6); + return _check(element, 7); } static bool isUint64Type(ClassElement element) { - return _check(element, 7); + return _check(element, 8); } } diff --git a/dart/packages/fory/lib/src/codegen/analyze/impl/struct/enum_analyzer_impl.dart b/dart/packages/fory/lib/src/codegen/analyze/impl/struct/enum_analyzer_impl.dart index 621b15ebdb..d4ce095cd2 100644 --- a/dart/packages/fory/lib/src/codegen/analyze/impl/struct/enum_analyzer_impl.dart +++ b/dart/packages/fory/lib/src/codegen/analyze/impl/struct/enum_analyzer_impl.dart @@ -17,26 +17,88 @@ * under the License. */ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:fory/src/codegen/analyze/analysis_type_identifier.dart'; import 'package:fory/src/codegen/analyze/interface/enum_analyzer.dart'; import 'package:fory/src/codegen/meta/impl/enum_spec_generator.dart'; class EnumAnalyzerImpl implements EnumAnalyzer { const EnumAnalyzerImpl(); + int? _readEnumId(FieldElement enumField) { + for (final ElementAnnotation annotation in enumField.metadata) { + final DartObject? annotationValue = annotation.computeConstantValue(); + final Element? typeElement = annotationValue?.type?.element; + if (typeElement is! ClassElement) { + continue; + } + if (!AnalysisTypeIdentifier.isForyEnumId(typeElement)) { + continue; + } + return annotationValue?.getField('id')?.toIntValue(); + } + return null; + } + @override EnumSpecGenerator analyze(EnumElement enumElement) { String packageName = enumElement.location!.components[0]; + final String enumName = enumElement.name; + final List enumFields = + enumElement.fields.where((FieldElement e) => e.isEnumConstant).toList(); + + final List enumValues = + enumFields.map((FieldElement e) => e.name).toList(); + + final Map enumIds = {}; + final Map usedIds = {}; + final List missingIdValues = []; + final List duplicateIds = []; + int annotatedCount = 0; + for (final FieldElement enumField in enumFields) { + final int? id = _readEnumId(enumField); + if (id == null) { + missingIdValues.add(enumField.name); + continue; + } + annotatedCount++; + + final String? firstValueWithId = usedIds[id]; + if (firstValueWithId != null) { + duplicateIds.add('$id for $firstValueWithId and ${enumField.name}'); + continue; + } + + usedIds[id] = enumField.name; + enumIds[enumField.name] = id; + } - List enumValues = enumElement.fields - .where((e) => e.isEnumConstant) - .map((e) => e.name) - .toList(); + final bool useAnnotatedIds = + missingIdValues.isEmpty && duplicateIds.isEmpty; + final bool hasAnyAnnotatedIds = annotatedCount > 0; + if (hasAnyAnnotatedIds && !useAnnotatedIds) { + if (missingIdValues.isNotEmpty) { + print( + '[WARNING] Enum $enumName in $packageName has partial @ForyEnumId annotations. ' + 'Missing values: ${missingIdValues.join(', ')}. ' + 'All @ForyEnumId annotations are ignored and ordinal serialization is used.', + ); + } + if (duplicateIds.isNotEmpty) { + print( + '[WARNING] Enum $enumName in $packageName has duplicate @ForyEnumId values ' + '(${duplicateIds.join('; ')}). ' + 'All @ForyEnumId annotations are ignored and ordinal serialization is used.', + ); + } + } return EnumSpecGenerator( - enumElement.name, + enumName, packageName, enumValues, + useAnnotatedIds ? enumIds : null, ); } } diff --git a/dart/packages/fory/lib/src/codegen/meta/impl/enum_spec_generator.dart b/dart/packages/fory/lib/src/codegen/meta/impl/enum_spec_generator.dart index c5d21755dd..b726447f6a 100644 --- a/dart/packages/fory/lib/src/codegen/meta/impl/enum_spec_generator.dart +++ b/dart/packages/fory/lib/src/codegen/meta/impl/enum_spec_generator.dart @@ -25,9 +25,15 @@ import 'package:meta/meta.dart'; @immutable class EnumSpecGenerator extends CustomTypeSpecGenerator { final List _enumVarNames; + final Map? _enumValueIds; late final String _varName; - EnumSpecGenerator(super.name, super.importPath, this._enumVarNames) { + EnumSpecGenerator( + super.name, + super.importPath, + this._enumVarNames, + this._enumValueIds, + ) { _varName = "\$$name"; assert(_enumVarNames.isNotEmpty); } @@ -45,6 +51,34 @@ class EnumSpecGenerator extends CustomTypeSpecGenerator { buf.write("],\n"); } + void _writeEnumIdMap(StringBuffer buf, int indentLevel) { + final Map? enumValueIds = _enumValueIds; + if (enumValueIds == null) { + return; + } + + final int totalIndent = indentLevel * CodegenStyle.indent; + CodegenTool.writeIndent(buf, totalIndent); + buf.write("{\n"); + + for (final String varName in _enumVarNames) { + final int? id = enumValueIds[varName]; + if (id == null) { + continue; + } + CodegenTool.writeIndent(buf, totalIndent + CodegenStyle.indent); + buf.write(id); + buf.write(": "); + buf.write(name); + buf.write("."); + buf.write(varName); + buf.write(",\n"); + } + + CodegenTool.writeIndent(buf, totalIndent); + buf.write("},\n"); + } + @override void writeCode(StringBuffer buf, [int indentLevel = 0]) { // buf.write(GenCodeStyle.magicSign); @@ -75,6 +109,7 @@ class EnumSpecGenerator extends CustomTypeSpecGenerator { // buf.write(",\n"); _writeFieldsStr(buf, indentLevel + 1); + _writeEnumIdMap(buf, indentLevel + 1); // tail part buf.write(");\n"); diff --git a/dart/packages/fory/lib/src/meta/specs/enum_spec.dart b/dart/packages/fory/lib/src/meta/specs/enum_spec.dart index 4a7cb969d7..52cbf78043 100644 --- a/dart/packages/fory/lib/src/meta/specs/enum_spec.dart +++ b/dart/packages/fory/lib/src/meta/specs/enum_spec.dart @@ -27,9 +27,9 @@ import 'package:fory/src/const/types.dart'; @immutable class EnumSpec extends CustomTypeSpec { // final String tag; - // TODO: Currently, enums only support using ordinal for transmission. There is also support for ForyEnum annotation, such as using value, so we can directly use the values array here. final List values; - const EnumSpec(Type dartType, this.values) + final Map? idToValue; + const EnumSpec(Type dartType, this.values, [this.idToValue]) : super(dartType, ObjType.NAMED_ENUM); @override @@ -38,7 +38,8 @@ class EnumSpec extends CustomTypeSpec { other is EnumSpec && runtimeType == other.runtimeType && dartType == other.dartType && - values.equals(other.values); + values.equals(other.values) && + const MapEquality().equals(idToValue, other.idToValue); } @override @@ -46,5 +47,6 @@ class EnumSpec extends CustomTypeSpec { runtimeType, dartType, values, + const MapEquality().hash(idToValue), ); } diff --git a/dart/packages/fory/lib/src/serializer/enum_serializer.dart b/dart/packages/fory/lib/src/serializer/enum_serializer.dart index 57d5903387..ab20c9a5a7 100644 --- a/dart/packages/fory/lib/src/serializer/enum_serializer.dart +++ b/dart/packages/fory/lib/src/serializer/enum_serializer.dart @@ -41,7 +41,7 @@ final class _EnumSerializerCache extends SerializerCache { return serializer; } // In foryJava, EnumSerializer does not perform reference tracking - serializer = EnumSerializer(false, spec.values); + serializer = EnumSerializer(false, spec.values, spec.idToValue); _cache[dartType] = serializer; return serializer; } @@ -51,22 +51,60 @@ final class EnumSerializer extends CustomSerializer { static const SerializerCache cache = _EnumSerializerCache(); final List values; - EnumSerializer(bool writeRef, this.values) - : super(ObjType.NAMED_ENUM, writeRef); + final Map? _idToValue; + final Map? _valueToId; + final List? _idCandidates; + + EnumSerializer(bool writeRef, this.values, [Map? idToValue]) + : _idToValue = idToValue, + _valueToId = idToValue == null + ? null + : { + for (final MapEntry entry in idToValue.entries) + entry.value: entry.key, + }, + _idCandidates = idToValue == null + ? null + : List.unmodifiable(idToValue.keys), + super(ObjType.NAMED_ENUM, writeRef); @override Enum read(ByteReader br, int refId, DeserializationContext pack) { - int index = br.readVarUint32Small7(); + final int indexOrId = br.readVarUint32Small7(); + final Map? idToValue = _idToValue; + if (idToValue != null) { + final Enum? enumValue = idToValue[indexOrId]; + if (enumValue == null) { + throw DeserializationRangeException(indexOrId, _idCandidates!); + } + return enumValue; + } // foryJava supports deserializeUnknownEnumValueAsNull, // but here in Dart, it will definitely throw an error if the index is out of range - if (index < 0 || index >= values.length) { - throw DeserializationRangeException(index, values); + // This is for the ordinal-based deserailization only when previous check fails + // and the variable here still means index, not id. + if (indexOrId < 0 || indexOrId >= values.length) { + throw DeserializationRangeException(indexOrId, values); } - return values[index]; + return values[indexOrId]; } @override void write(ByteWriter bw, Enum v, SerializationContext pack) { + final Map? valueToId = _valueToId; + if (valueToId != null) { + final int? id = valueToId[v]; + if (id == null) { + throw ArgumentError.value( + v, + 'v', + 'Enum value is missing from EnumSpec.idToValue mapping.', + ); + } + bw.writeVarUint32Small7(id); + return; + } + bw.writeVarUint32Small7(v.index); } }