Skip to content
Merged
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
35 changes: 35 additions & 0 deletions wurst/file/SerializableTests.wurst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
25 changes: 24 additions & 1 deletion wurst/file/StructuredSerialization.wurst
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines +214 to +218

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the older field-intrinsic aliases

When consumers use a compiler predating the canonical #1224 intrinsic names, these wurstForFields/wurstMapFields calls are not lowered, so any class adopting the new module fails to compile. This package explicitly preserves those compiler versions immediately below in SerializableFields by calling the recognized forFields/mapFields aliases; use the same aliases here so the new public module retains that compatibility.

AGENTS.md reference: AGENTS.md:L3-L6

Useful? React with 👍 / 👎.


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.

Expand All @@ -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
Expand Down