diff --git a/wurst/file/SerializableTests.wurst b/wurst/file/SerializableTests.wurst index 5910a7f6..3ea3a14f 100644 --- a/wurst/file/SerializableTests.wurst +++ b/wurst/file/SerializableTests.wurst @@ -98,6 +98,20 @@ class OldPlayerState extends Serializable override function deserializeProperties() score = getIntProperty("score") +class TransitionalPlayerState extends Serializable implements FieldSerializable + use SerializableFieldMapping + + int score = 0 + string title = "default" + + override function serializeProperties() + addProperty("score", score) + addProperty("title", title) + + override function deserializeProperties() + score = getIntProperty("score") + title = getStringProperty("title") + @Test function reflectedFieldsRoundTripNestedClassesAndTuples() constructions = 0 @@ -128,6 +142,27 @@ function reflectedFieldsRoundTripNestedClassesAndTuples() destroy loaded destroy data +@Test +function mappingOnlyModuleCoexistsWithLegacySerializable() + let original = new TransitionalPlayerState() + original.score = 81 + original.title = "transition" + let writer = new FieldSerializationWriter(1) + writer.write("profile", original) + let data = writer.finish() + let reader = new FieldSerializationReader(data) + let loaded = new TransitionalPlayerState() + + reader.readInto("profile", loaded).assertTrue() + loaded.score.assertEquals(81) + loaded.title.assertEquals("transition") + + destroy loaded + destroy reader + destroy data + destroy writer + destroy original + @Test function customTupleCodecRoundTripsThroughSerializableFields() let original = new CustomTupleState() diff --git a/wurst/file/StructuredSerialization.wurst b/wurst/file/StructuredSerialization.wurst index 19f99272..5c5caaae 100644 --- a/wurst/file/StructuredSerialization.wurst +++ b/wurst/file/StructuredSerialization.wurst @@ -202,6 +202,28 @@ public module FieldSerializableLifecycle deserialize(input) return this +/** +Automatic field mapping without adding a second `serialize`/`deserialize` lifecycle. + +Use this during a compatibility window when a data class still extends legacy `Serializable`, or +when another base class already owns lifecycle methods. The class must implement `FieldSerializable`. +All of its accessible mutable instance fields participate; keep runtime-only state in a separate +class because persisted-field annotation filtering is not available. +*/ +public module SerializableFieldMapping + function writeSerializedFields(FieldSerializationWriter writer) + wurstForFields((fieldName, value) -> writer.write(fieldName, value)) + + function readSerializedFields(FieldSerializationReader reader) + wurstMapFields((fieldName, value) -> value.readSerializedField(reader, fieldName)) + + function readSerializedField(FieldSerializationReader reader, string name) returns thistype + let child = reader.readObject(name) + if child.isValid() + readSerializedFields(child) + destroy child + return this + /** Modern automatic serialization for dedicated state/DAO classes using built-in codecs. @@ -210,7 +232,8 @@ iteration to direct accesses. The tagged format tolerates field reordering, adde and unknown future wire types. Missing fields retain constructor defaults. Use a schema version and `SerializationMigration` for semantic changes or `renameField` for renamed attributes. For custom tuple codecs declared in the consuming package, use `FieldSerializableLifecycle` as documented -above so overload resolution occurs where those codecs are visible. +above so overload resolution occurs where those codecs are visible. Classes that must also retain +legacy `Serializable` should use `SerializableFieldMapping` to avoid conflicting lifecycle methods. */ public module SerializableFields use FieldSerializableLifecycle