Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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 'dart:typed_data';
import 'package:fory/fory.dart';
import 'package:fory_test/entity/simple_struct1.dart';
import 'package:test/test.dart';

const int _listTypeId = 22;
const int _namedCompatibleStructTypeId = 30;

void main() {
group('Schema evolution (TypeDef meta share)', () {
test('round-trip with compatible mode and meta share', () {
final fory = Fory(compatible: true, ref: true);
fory.register($SimpleStruct1, typename: 'SimpleStruct1');

final a = SimpleStruct1()..a = Int32(100);
final b = SimpleStruct1()..a = Int32(200);

final bytes = fory.serialize([a, b]);
expect(bytes.isNotEmpty, isTrue);

final decoded = fory.deserialize(bytes) as List<Object?>;
expect(decoded.length, 2);
expect(decoded[0], isA<SimpleStruct1>());
expect(decoded[1], isA<SimpleStruct1>());
expect((decoded[0] as SimpleStruct1).a.value, 100);
expect((decoded[1] as SimpleStruct1).a.value, 200);
});

test('meta share cache: first occurrence has TypeDef, later use reference', () {
final fory = Fory(compatible: true, ref: true);
fory.register($SimpleStruct1, typename: 'SimpleStruct1');

final list = [
SimpleStruct1()..a = Int32(1),
SimpleStruct1()..a = Int32(2),
SimpleStruct1()..a = Int32(3),
];
final bytes = fory.serialize(list);

final markers = _collectSharedTypeMarkers(bytes);
expect(markers.length, 1, reason: 'List writes element type once');
expect(markers[0] & 1, 0, reason: 'Only occurrence should be new TypeDef (even marker)');

final decoded = fory.deserialize(bytes) as List<Object?>;
expect(decoded.length, 3);
expect((decoded[0] as SimpleStruct1).a.value, 1);
expect((decoded[1] as SimpleStruct1).a.value, 2);
expect((decoded[2] as SimpleStruct1).a.value, 3);
});

test('TypeDef format: type ID and field list round-trip', () {
final fory = Fory(compatible: true, ref: true);
fory.register($SimpleStruct1, typename: 'test.SimpleStruct1');

final original = SimpleStruct1()..a = Int32(42);
final bytes = fory.serialize(original);
final decoded = fory.deserialize(bytes) as SimpleStruct1;

expect(decoded.a.value, 42);
});

test('named compatible struct (typename) round-trip', () {
final fory = Fory(compatible: true, ref: true);
fory.register($SimpleStruct1, typename: 'myns.SimpleStruct1');

final original = SimpleStruct1()..a = Int32(123);
final bytes = fory.serialize(original);
final decoded = fory.deserialize(bytes) as SimpleStruct1;
expect(decoded.a.value, 123);
});

test('compatible struct by user type id round-trip', () {
final fory = Fory(compatible: true, ref: true);
fory.register($SimpleStruct1, typeId: 1000);

final original = SimpleStruct1()..a = Int32(456);
final bytes = fory.serialize(original);
final decoded = fory.deserialize(bytes) as SimpleStruct1;
expect(decoded.a.value, 456);
});

test(
'cross-language golden: decode bytes from Java/Go/Python with different field order',
() {
expect(true, isTrue, reason: 'Placeholder: add golden file and decode assertion');
},
skip: 'Cross-language golden bytes not yet added; use integration test with Java/Go/Python',
);
});
}

List<int> _collectSharedTypeMarkers(Uint8List bytes) {
const int metaSizeMask = 0xFF;
final markers = <int>[];
final br = ByteReader.forBytes(bytes);
br.skip(1); // global header
final refFlag = br.readInt8();
if (refFlag < 0) return markers;
final typeId = br.readVarUint32Small7();
if (typeId != _listTypeId) return markers;
final length = br.readVarUint32Small7();
if (length == 0) return markers;
br.readUint8();
final elemTypeId = br.readVarUint32Small7();
if (elemTypeId == _namedCompatibleStructTypeId) {
final marker = br.readVarUint32();
markers.add(marker);
if ((marker & 1) == 0) {
final id = br.readInt64();
int size = id & metaSizeMask;
if (size == metaSizeMask) size += br.readVarUint32();
br.skip(size);
}
}
return markers;
}
5 changes: 4 additions & 1 deletion dart/packages/fory/lib/src/deserialization_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import 'package:fory/src/deserialization_dispatcher.dart';
import 'package:fory/src/meta/field_def.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/type_resolver.dart';
Expand All @@ -35,7 +36,9 @@ final class DeserializationContext extends Pack {

final Stack<TypeSpecWrap> typeWrapStack;

const DeserializationContext(
List<FieldDef>? currentRemoteFieldDefs;

DeserializationContext(
super.structHashResolver,
super.getTagByDartType,
this.header,
Expand Down
27 changes: 24 additions & 3 deletions dart/packages/fory/lib/src/deserialization_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class DeserializationDispatcher {
HeaderBrief? header = _foryHeaderSerializer.read(br, conf);
if (header == null) return null;
typeResolver.resetReadContext();

DeserializationContext deserializationContext = DeserializationContext(
StructHashResolver.inst,
typeResolver.getRegisteredTag,
Expand All @@ -79,7 +78,7 @@ class DeserializationDispatcher {
}
if (refFlag >= RefFlag.UNTRACKED_NOT_NULL.id) {
// must deserialize
TypeInfo typeInfo = pack.typeResolver.readTypeInfo(br);
TypeInfo typeInfo = pack.typeResolver.readTypeInfo(br, pack);
int refId = refResolver.reserveId();
Object o = _readByTypeInfo(br, typeInfo, refId, pack);
refResolver.setRef(refId, o);
Expand Down Expand Up @@ -117,10 +116,32 @@ class DeserializationDispatcher {
}

Object readDynamicWithoutRef(ByteReader br, DeserializationContext pack) {
TypeInfo typeInfo = pack.typeResolver.readTypeInfo(br);
TypeInfo typeInfo = pack.typeResolver.readTypeInfo(br, pack);
return _readByTypeInfo(br, typeInfo, -1, pack);
}

Object? readByTypeInfo(ByteReader br, TypeInfo typeInfo, int refId,
DeserializationContext pack,
{bool? trackingRefOverride}) {
bool trackingRef = trackingRefOverride ?? typeInfo.serializer.writeRef;
if (trackingRef) {
DeserializationRefResolver refResolver = pack.refResolver;
int refFlag = br.readInt8();
if (refFlag == RefFlag.NULL.id) return null;
if (refFlag == RefFlag.TRACKED_ALREADY.id) {
int refId = br.readVarUint32Small14();
return refResolver.getObj(refId);
}
if (refFlag >= RefFlag.UNTRACKED_NOT_NULL.id) {
int actualRefId = refResolver.reserveId();
Object o = _readByTypeInfo(br, typeInfo, actualRefId, pack);
refResolver.setRef(actualRefId, o);
return o;
}
}
return _readByTypeInfo(br, typeInfo, refId, pack);
}

Object _readByTypeInfo(ByteReader br, TypeInfo typeInfo, int refId,
DeserializationContext pack) {
switch (typeInfo.objType) {
Expand Down
70 changes: 70 additions & 0 deletions dart/packages/fory/lib/src/meta/field_def.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.
*/

const int kTagIdUseFieldName = -1;

class FieldDef {
final String name;
final int tagId;
final bool nullable;
final bool trackingRef;
final RemoteFieldType fieldType;

const FieldDef({
required this.name,
required this.tagId,
required this.nullable,
required this.trackingRef,
required this.fieldType,
});
}

/// Type of a field as read from TypeDef (supports nested LIST/SET/MAP).
abstract base class RemoteFieldType {
const RemoteFieldType();

int get typeId;
}

/// Simple (non-collection) field type.
final class SimpleRemoteFieldType extends RemoteFieldType {
@override
final int typeId;

const SimpleRemoteFieldType(this.typeId);
}

/// LIST or SET: typeId is LIST.id or SET.id, elementType is the element type.
final class ListSetRemoteFieldType extends RemoteFieldType {
@override
final int typeId;
final RemoteFieldType elementType;

const ListSetRemoteFieldType(this.typeId, this.elementType);
}

/// MAP: typeId is MAP.id, keyType and valueType are the key/value types.
final class MapRemoteFieldType extends RemoteFieldType {
@override
final int typeId;
final RemoteFieldType keyType;
final RemoteFieldType valueType;

const MapRemoteFieldType(this.typeId, this.keyType, this.valueType);
}
11 changes: 11 additions & 0 deletions dart/packages/fory/lib/src/meta/spec_wraps/type_spec_wrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ class TypeSpecWrap {
return typeSpecWraps;
}

static TypeSpecWrap forRemote(Serializer serializer, {bool nullable = false}) {
return TypeSpecWrap._(
Object,
serializer.objType,
true,
nullable,
const [],
serializer,
);
}

bool get hasGenericsParam => genericsArgs.isNotEmpty;

TypeSpecWrap? get param0 => genericsArgs.isNotEmpty ? genericsArgs[0] : null;
Expand Down
Loading
Loading