From 006af57acc1871847f3172a0e16facaaeee68ae2 Mon Sep 17 00:00:00 2001 From: sebaciv Date: Sat, 21 Sep 2024 16:41:01 +0200 Subject: [PATCH 1/8] Implement IgnoreTransientDefaultMarker --- .../IgnoreTransientDefaultMarker.scala | 15 ++ .../serialization/customMarkerWrappers.scala | 139 ++++++++++++++++++ .../IgnoreTransientDefaultMarkerTest.scala | 52 +++++++ .../macros/serialization/GenCodecMacros.scala | 33 ++++- 4 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala create mode 100644 core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala create mode 100644 core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala diff --git a/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala b/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala new file mode 100644 index 000000000..5117a1b9d --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala @@ -0,0 +1,15 @@ +package com.avsystem.commons +package serialization + +/** + * Instructs [[GenCodec]] to ignore the [[transientDefault]] annotation when serializing a case class. + * This ensures that even if a field's value is the same as its default, it will be included in the serialized + * representation. Deserialization behavior remains unchanged. If a field is missing from the input, the default + * value will be used as usual. + * + * This annotation can be helpful when using the same model class in multiple contexts with different serialization + * formats that have conflicting requirements for handling default values. + * + * @see [[CustomMarkersOutputWrapper]] + */ +object IgnoreTransientDefaultMarker extends CustomEventMarker[Unit] diff --git a/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala new file mode 100644 index 000000000..28f194c01 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala @@ -0,0 +1,139 @@ +package com.avsystem.commons +package serialization + +/** + * [[Input]] implementation that adds additional markers [[CustomEventMarker]] to the provided [[Input]] instance + */ +final class CustomMarkersInputWrapper( + override protected val wrapped: Input, + markers: Set[CustomEventMarker[_]], +) extends InputWrapper { + + override def readList(): ListInput = + new CustomMarkersInputWrapper.AdjustedListInput(super.readList(), markers) + + override def readObject(): ObjectInput = + new CustomMarkersInputWrapper.AdjustedObjectInput(super.readObject(), markers) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } +} +object CustomMarkersInputWrapper { + def apply(input: Input, markers: CustomEventMarker[_]*): CustomMarkersInputWrapper = + new CustomMarkersInputWrapper(input, markers.toSet) + + private final class AdjustedListInput( + override protected val wrapped: ListInput, + markers: Set[CustomEventMarker[_]], + ) extends ListInputWrapper { + override def nextElement(): Input = new CustomMarkersInputWrapper(super.nextElement(), markers) + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } + + private final class AdjustedFieldInput( + override protected val wrapped: FieldInput, + markers: Set[CustomEventMarker[_]], + ) extends FieldInputWrapper { + + override def readList(): ListInput = new AdjustedListInput(super.readList(), markers) + override def readObject(): ObjectInput = new AdjustedObjectInput(super.readObject(), markers) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } + + private final class AdjustedObjectInput( + override protected val wrapped: ObjectInput, + markers: Set[CustomEventMarker[_]], + ) extends ObjectInputWrapper { + + override def nextField(): FieldInput = new AdjustedFieldInput(super.nextField(), markers) + + override def peekField(name: String): Opt[FieldInput] = + super.peekField(name).map(new AdjustedFieldInput(_, markers)) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } +} + +/** + * [[Output]] implementation that adds additional markers [[CustomEventMarker]] to the provided [[Output]] instance + */ +final class CustomMarkersOutputWrapper( + override protected val wrapped: Output, + markers: Set[CustomEventMarker[_]], +) extends OutputWrapper { + + override def writeSimple(): SimpleOutput = + new CustomMarkersOutputWrapper.AdjustedSimpleOutput(super.writeSimple(), markers) + + override def writeList(): ListOutput = + new CustomMarkersOutputWrapper.AdjustedListOutput(super.writeList(), markers) + + override def writeObject(): ObjectOutput = + new CustomMarkersOutputWrapper.AdjustedObjectOutput(super.writeObject(), markers) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } +} + +object CustomMarkersOutputWrapper { + def apply(output: Output, markers: CustomEventMarker[_]*): CustomMarkersOutputWrapper = + new CustomMarkersOutputWrapper(output, markers.toSet) + + private final class AdjustedSimpleOutput( + override protected val wrapped: SimpleOutput, + markers: Set[CustomEventMarker[_]], + ) extends SimpleOutputWrapper { + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } + + private final class AdjustedListOutput( + override protected val wrapped: ListOutput, + markers: Set[CustomEventMarker[_]], + ) extends ListOutputWrapper { + override def writeElement(): Output = + new CustomMarkersOutputWrapper(super.writeElement(), markers) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } + + private final class AdjustedObjectOutput( + override protected val wrapped: ObjectOutput, + markers: Set[CustomEventMarker[_]], + ) extends ObjectOutputWrapper { + override def writeField(key: String): Output = + new CustomMarkersOutputWrapper(super.writeField(key), markers) + + override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, value) + } + } +} diff --git a/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala new file mode 100644 index 000000000..8032a8b0a --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala @@ -0,0 +1,52 @@ +package com.avsystem.commons +package serialization + +import com.avsystem.commons.serialization.CodecTestData.HasDefaults + +object IgnoreTransientDefaultMarkerTest { + final case class NestedHasDefaults( + @transientDefault flag: Boolean = false, + obj: HasDefaults, + list: Seq[HasDefaults], + @transientDefault defaultObj: HasDefaults = HasDefaults(), + ) + object NestedHasDefaults extends HasGenCodec[NestedHasDefaults] +} + +class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { + import IgnoreTransientDefaultMarkerTest._ + + override type Raw = Any + + def writeToOutput(write: Output => Unit): Any = { + var result: Any = null + write(CustomMarkersOutputWrapper(new SimpleValueOutput(result = _), IgnoreTransientDefaultMarker)) + result + } + + def createInput(raw: Any): Input = new SimpleValueInput(raw) + + test("case class with default values") { + testWrite(HasDefaults(str = "lol"), Map("str" -> "lol", "int" -> 42)) + testWrite(HasDefaults(43, "lol"), Map("int" -> 43, "str" -> "lol")) + testWrite(HasDefaults(str = null), Map("str" -> null, "int" -> 42)) + testWrite(HasDefaults(str = "dafuq"), Map("str" -> "dafuq", "int" -> 42)) + } + + test("nested case class with default values") { + testWrite( + value = NestedHasDefaults( + flag = false, + obj = HasDefaults(str = "lol"), + list = Seq(HasDefaults(int = 43)), + defaultObj = HasDefaults(), + ), + expectedRepr = Map( + "flag" -> false, + "defaultObj" -> Map[String, Any]("str" -> "kek", "int" -> 42), + "obj" -> Map[String, Any]("str" -> "lol", "int" -> 42), + "list" -> List(Map[String, Any]("str" -> "kek", "int" -> 43)), + ), + ) + } +} diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index 3ae6fc7a3..f75e056e9 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -177,11 +177,20 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with if (isTransientDefault(p)) Some(p.defaultValue) else p.optionLike.map(ol => q"${ol.reference(Nil)}.none") - val writeArgs = q"output" :: q"${p.idx}" :: value :: transientValue.toList + val writeArgsNoTransient = q"output" :: q"${p.idx}" :: List(value) + val writeArgs = writeArgsNoTransient ::: transientValue.toList val writeTargs = if (isOptimizedPrimitive(p)) Nil else List(p.valueType) - q"writeField[..$writeTargs](..$writeArgs)" + q""" + if (ignoreTransientDefault) + writeField[..$writeTargs](..$writeArgsNoTransient) + else + writeField[..$writeTargs](..$writeArgs) + """ } + def ignoreTransientDefaultCheck: Tree = + q"val ignoreTransientDefault = output.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + def writeFields: Tree = params match { case Nil => if (canUseFields) @@ -194,20 +203,32 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with """ case List(p: ApplyParam) => if (canUseFields) - writeField(p, q"value.${p.sym.name}") + q""" + $ignoreTransientDefaultCheck + ${writeField(p, q"value.${p.sym.name}")} + """ else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if(unapplyRes.isEmpty) unapplyFailed else ${writeField(p, q"unapplyRes.get")} + if (unapplyRes.isEmpty) unapplyFailed + else { + $ignoreTransientDefaultCheck + ${writeField(p, q"unapplyRes.get")} + } """ case _ => if (canUseFields) - q"..${params.map(p => writeField(p, q"value.${p.sym.name}"))}" + q""" + $ignoreTransientDefaultCheck + ..${params.map(p => writeField(p, q"value.${p.sym.name}"))} + """ else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if(unapplyRes.isEmpty) unapplyFailed else { + if (unapplyRes.isEmpty) unapplyFailed + else { val t = unapplyRes.get + $ignoreTransientDefaultCheck ..${params.map(p => writeField(p, q"t.${tupleGet(p.idx)}"))} } """ From 3982060043775a226f84ee2b7aa2e890b303c306 Mon Sep 17 00:00:00 2001 From: sebaciv Date: Sat, 21 Sep 2024 17:39:11 +0200 Subject: [PATCH 2/8] Implement IgnoreTransientDefaultMarker - improve doc --- .../commons/serialization/IgnoreTransientDefaultMarker.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala b/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala index 5117a1b9d..a80b30808 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarker.scala @@ -7,9 +7,9 @@ package serialization * representation. Deserialization behavior remains unchanged. If a field is missing from the input, the default * value will be used as usual. * - * This annotation can be helpful when using the same model class in multiple contexts with different serialization + * This marker can be helpful when using the same model class in multiple contexts with different serialization * formats that have conflicting requirements for handling default values. * - * @see [[CustomMarkersOutputWrapper]] + * @see [[CustomMarkersOutputWrapper]] for an easy way to add markers to existing [[Output]] implementations */ object IgnoreTransientDefaultMarker extends CustomEventMarker[Unit] From 4276c3699a1e5e5d95cf0e9e426e6615f0037794 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 10:36:42 +0200 Subject: [PATCH 3/8] Deduplicate customEvent method code --- .../serialization/customMarkerWrappers.scala | 93 ++++++------------- 1 file changed, 29 insertions(+), 64 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala index 28f194c01..4a5e32253 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala @@ -1,25 +1,30 @@ package com.avsystem.commons package serialization +trait AcceptsAdditionalCustomMarkers extends AcceptsCustomEvents { + + protected def markers: Set[CustomEventMarker[_]] + + override def customEvent[T](marker: CustomEventMarker[T], event: T): Boolean = + marker match { + case marker if markers(marker) => true + case _ => super.customEvent(marker, event) + } +} + /** * [[Input]] implementation that adds additional markers [[CustomEventMarker]] to the provided [[Input]] instance */ final class CustomMarkersInputWrapper( override protected val wrapped: Input, - markers: Set[CustomEventMarker[_]], -) extends InputWrapper { + override protected val markers: Set[CustomEventMarker[_]], +) extends InputWrapper with AcceptsAdditionalCustomMarkers { override def readList(): ListInput = new CustomMarkersInputWrapper.AdjustedListInput(super.readList(), markers) override def readObject(): ObjectInput = new CustomMarkersInputWrapper.AdjustedObjectInput(super.readObject(), markers) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } object CustomMarkersInputWrapper { def apply(input: Input, markers: CustomEventMarker[_]*): CustomMarkersInputWrapper = @@ -27,46 +32,28 @@ object CustomMarkersInputWrapper { private final class AdjustedListInput( override protected val wrapped: ListInput, - markers: Set[CustomEventMarker[_]], - ) extends ListInputWrapper { + override protected val markers: Set[CustomEventMarker[_]], + ) extends ListInputWrapper with AcceptsAdditionalCustomMarkers { override def nextElement(): Input = new CustomMarkersInputWrapper(super.nextElement(), markers) - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } private final class AdjustedFieldInput( override protected val wrapped: FieldInput, - markers: Set[CustomEventMarker[_]], - ) extends FieldInputWrapper { + override protected val markers: Set[CustomEventMarker[_]], + ) extends FieldInputWrapper with AcceptsAdditionalCustomMarkers { override def readList(): ListInput = new AdjustedListInput(super.readList(), markers) override def readObject(): ObjectInput = new AdjustedObjectInput(super.readObject(), markers) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } private final class AdjustedObjectInput( override protected val wrapped: ObjectInput, - markers: Set[CustomEventMarker[_]], - ) extends ObjectInputWrapper { + override protected val markers: Set[CustomEventMarker[_]], + ) extends ObjectInputWrapper with AcceptsAdditionalCustomMarkers { override def nextField(): FieldInput = new AdjustedFieldInput(super.nextField(), markers) - override def peekField(name: String): Opt[FieldInput] = super.peekField(name).map(new AdjustedFieldInput(_, markers)) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } } @@ -75,8 +62,8 @@ object CustomMarkersInputWrapper { */ final class CustomMarkersOutputWrapper( override protected val wrapped: Output, - markers: Set[CustomEventMarker[_]], -) extends OutputWrapper { + override protected val markers: Set[CustomEventMarker[_]], +) extends OutputWrapper with AcceptsAdditionalCustomMarkers { override def writeSimple(): SimpleOutput = new CustomMarkersOutputWrapper.AdjustedSimpleOutput(super.writeSimple(), markers) @@ -86,12 +73,6 @@ final class CustomMarkersOutputWrapper( override def writeObject(): ObjectOutput = new CustomMarkersOutputWrapper.AdjustedObjectOutput(super.writeObject(), markers) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } object CustomMarkersOutputWrapper { @@ -100,40 +81,24 @@ object CustomMarkersOutputWrapper { private final class AdjustedSimpleOutput( override protected val wrapped: SimpleOutput, - markers: Set[CustomEventMarker[_]], - ) extends SimpleOutputWrapper { - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } - } + override protected val markers: Set[CustomEventMarker[_]], + ) extends SimpleOutputWrapper with AcceptsAdditionalCustomMarkers private final class AdjustedListOutput( override protected val wrapped: ListOutput, - markers: Set[CustomEventMarker[_]], - ) extends ListOutputWrapper { + override protected val markers: Set[CustomEventMarker[_]], + ) extends ListOutputWrapper with AcceptsAdditionalCustomMarkers { + override def writeElement(): Output = new CustomMarkersOutputWrapper(super.writeElement(), markers) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } private final class AdjustedObjectOutput( override protected val wrapped: ObjectOutput, - markers: Set[CustomEventMarker[_]], - ) extends ObjectOutputWrapper { + override protected val markers: Set[CustomEventMarker[_]], + ) extends ObjectOutputWrapper with AcceptsAdditionalCustomMarkers { + override def writeField(key: String): Output = new CustomMarkersOutputWrapper(super.writeField(key), markers) - - override def customEvent[T](marker: CustomEventMarker[T], value: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, value) - } } } From 37c2a77643a10619633107eadf448b438b4cabdd Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 13:07:19 +0200 Subject: [PATCH 4/8] Handle com.avsystem.commons.serialization.GenCodec.SizedCodec.size with IgnoreTransientDefaultMarker --- .../commons/serialization/GenCodec.scala | 10 ++-- .../cbor/CborOptimizedCodecs.scala | 2 +- .../commons/serialization/macroCodecs.scala | 4 +- .../serialization/ObjectSizeTest.scala | 15 ++++++ .../cbor/CborInputOutputTest.scala | 48 ++++++++++++++++--- .../macros/serialization/GenCodecMacros.scala | 4 +- 6 files changed, 68 insertions(+), 15 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala b/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala index 123ca6728..b35f06563 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala @@ -312,11 +312,13 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { } trait SizedCodec[T] extends GenCodec[T] { - def size(value: T): Int + def size(value: T): Int = size(value, Opt.Empty) + + def size(value: T, output: Opt[SequentialOutput]): Int protected final def declareSizeFor(output: SequentialOutput, value: T): Unit = if (output.sizePolicy != SizePolicy.Ignored) { - output.declareSize(size(value)) + output.declareSize(size(value, output.opt)) } } @@ -336,8 +338,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { object OOOFieldsObjectCodec { // this was introduced so that transparent wrapper cases are possible in flat sealed hierarchies final class Transformed[A, B](val wrapped: OOOFieldsObjectCodec[B], onWrite: A => B, onRead: B => A) extends OOOFieldsObjectCodec[A] { - def size(value: A): Int = - wrapped.size(onWrite(value)) + def size(value: A, output: Opt[SequentialOutput]): Int = + wrapped.size(onWrite(value), output) def readObject(input: ObjectInput, outOfOrderFields: FieldValues): A = onRead(wrapped.readObject(input, outOfOrderFields)) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/cbor/CborOptimizedCodecs.scala b/core/src/main/scala/com/avsystem/commons/serialization/cbor/CborOptimizedCodecs.scala index d46a06454..a7e168778 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/cbor/CborOptimizedCodecs.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/cbor/CborOptimizedCodecs.scala @@ -87,7 +87,7 @@ class OOOFieldCborRawKeysCodec[T](stdObjectCodec: OOOFieldsObjectCodec[T], keyCo stdObjectCodec.writeFields(output, value) } - def size(value: T): Int = stdObjectCodec.size(value) + def size(value: T, output: Opt[SequentialOutput]): Int = stdObjectCodec.size(value, output) def nullable: Boolean = stdObjectCodec.nullable } diff --git a/core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala b/core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala index 9475bf5cf..77ced836b 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala @@ -12,7 +12,7 @@ class SingletonCodec[T <: Singleton]( ) extends ErrorReportingCodec[T] with OOOFieldsObjectCodec[T] { final def nullable = true final def readObject(input: ObjectInput, outOfOrderFields: FieldValues): T = singletonValue - def size(value: T): Int = 0 + def size(value: T, output: Opt[SequentialOutput]): Int = 0 def writeFields(output: ObjectOutput, value: T): Unit = () } @@ -109,7 +109,7 @@ abstract class ProductCodec[T <: Product]( nullable: Boolean, fieldNames: Array[String] ) extends ApplyUnapplyCodec[T](typeRepr, nullable, fieldNames) { - def size(value: T): Int = value.productArity + def size(value: T, output: Opt[SequentialOutput]): Int = value.productArity final def writeFields(output: ObjectOutput, value: T): Unit = { val size = value.productArity diff --git a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala index 62faf1ac4..415d706f7 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala @@ -34,4 +34,19 @@ class ObjectSizeTest extends AnyFunSuite { assert(CustomWrapper.codec.size(CustomWrapper()) == 0) assert(CustomWrapper.codec.size(CustomWrapper("fuu")) == 1) } + + test("computing object size with custom output") { + val defaultIgnoringOutput = new SequentialOutput { + override def customEvent[T](marker: CustomEventMarker[T], event: T): Boolean = + marker match { + case IgnoreTransientDefaultMarker => true + case _ => super.customEvent(marker, event) + } + override def finish(): Unit = () + } + assert(RecordWithDefaults.codec.size(RecordWithDefaults(), defaultIgnoringOutput.opt) == 3) + assert(RecordWithDefaults.codec.size(RecordWithDefaults("fuu"), defaultIgnoringOutput.opt) == 3) + assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults(), defaultIgnoringOutput.opt) == 2) + assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults("fuu"), defaultIgnoringOutput.opt) == 2) + } } diff --git a/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala index fa77ef529..dbc4500fe 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala @@ -28,6 +28,18 @@ case class CustomKeysRecord( ) object CustomKeysRecord extends HasCborCodec[CustomKeysRecord] +case class CustomKeysRecordWithDefaults( + @transientDefault @cborKey(1) first: Int = 0, + @cborKey(true) second: Boolean, +) +object CustomKeysRecordWithDefaults extends HasCborCodec[CustomKeysRecordWithDefaults] + +case class CustomKeysRecordWithNoDefaults( + @cborKey(1) first: Int = 0, + @cborKey(true) second: Boolean, +) +object CustomKeysRecordWithNoDefaults extends HasCborCodec[CustomKeysRecordWithNoDefaults] + @cborDiscriminator(0) sealed trait GenericSealedTrait[+T] object GenericSealedTrait extends HasPolyCborCodec[GenericSealedTrait] { @@ -61,14 +73,22 @@ class CborInputOutputTest extends AnyFunSuite { keyCodec: CborKeyCodec = CborKeyCodec.Default )(implicit pos: Position): Unit = test(s"${pos.lineNumber}: $value") { - val baos = new ByteArrayOutputStream - val output = new CborOutput(new DataOutputStream(baos), keyCodec, SizePolicy.Optional) - GenCodec.write[T](output, value) - val bytes = baos.toByteArray - assert(Bytes(bytes).toString == binary) - assert(RawCbor(bytes).readAs[T](keyCodec) == value) + assertRoundtrip(value, binary, keyCodec) } + private def assertRoundtrip[T: GenCodec]( + value: T, + binary: String, + keyCodec: CborKeyCodec = CborKeyCodec.Default + )(implicit pos: Position): Unit = { + val baos = new ByteArrayOutputStream + val output = new CborOutput(new DataOutputStream(baos), keyCodec, SizePolicy.Optional) + GenCodec.write[T](output, value) + val bytes = baos.toByteArray + assert(Bytes(bytes).toString == binary) + assert(RawCbor(bytes).readAs[T](keyCodec) == value) + } + // binary representation from cbor.me roundtrip(null, "F6") @@ -213,6 +233,22 @@ class CborInputOutputTest extends AnyFunSuite { """{"first":42,"second":true,"third":"foo","strMap":{"foo":1},"intMap":{"1":"foo"}}""") } + test("writing with IgnoreTransientDefaultMarker to CBOR output") { + val baos = new ByteArrayOutputStream + val output = CustomMarkersOutputWrapper( + new CborOutput(new DataOutputStream(baos), keyCodec, SizePolicy.Optional), + IgnoreTransientDefaultMarker, + ) + val value = CustomKeysRecordWithDefaults(first = 0, second = true) + GenCodec.write(output, value) + val bytes = Bytes(baos.toByteArray) + assert(bytes.toString == "A20100F5F5") + assert(RawCbor(bytes.bytes).readAs[CustomKeysRecordWithDefaults](keyCodec) == value) + + // should be the same as model with @transientDefault and serialization ignoring it + assertRoundtrip(CustomKeysRecordWithNoDefaults(first = 0, second = true), "A20100F5F5") + } + test("chunked text string") { assert(CborInput.readRawCbor[String](RawCbor.fromHex("7F626162626162626162FF")) == "ababab") } diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index f75e056e9..a186d3aff 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -313,8 +313,8 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with def sizeMethod: List[Tree] = if (useProductCodec) Nil else { val res = q""" - def size(value: $dtpe): $IntCls = - ${params.size} + ${generated.size} - $countTransientFields + def size(value: $dtpe, output: $OptCls[$SerializationPkg.SequentialOutput]): $IntCls = + ${params.size} + ${generated.size} - output.filter(_.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())).mapOr($countTransientFields, _ => 0) """ List(res) } From b19248f0b49542ec29422277b7e9bef5d6773a7f Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 18:50:29 +0200 Subject: [PATCH 5/8] Optimize macro code when handling IgnoreTransientDefaultMarker --- .../IgnoreTransientDefaultMarkerTest.scala | 27 +++++++- .../macros/serialization/GenCodecMacros.scala | 69 ++++++++++++------- 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala index 8032a8b0a..30fe2c0b2 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala @@ -11,6 +11,12 @@ object IgnoreTransientDefaultMarkerTest { @transientDefault defaultObj: HasDefaults = HasDefaults(), ) object NestedHasDefaults extends HasGenCodec[NestedHasDefaults] + + final case class HasOptParam( + @transientDefault flag: Boolean = false, + @optionalParam str: Opt[String] = Opt.Empty, + ) + object HasOptParam extends HasGenCodec[HasOptParam] } class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { @@ -24,16 +30,31 @@ class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { result } - def createInput(raw: Any): Input = new SimpleValueInput(raw) + def createInput(raw: Any): Input = + CustomMarkersInputWrapper(new SimpleValueInput(raw), IgnoreTransientDefaultMarker) - test("case class with default values") { + test("write case class with default values") { testWrite(HasDefaults(str = "lol"), Map("str" -> "lol", "int" -> 42)) testWrite(HasDefaults(43, "lol"), Map("int" -> 43, "str" -> "lol")) testWrite(HasDefaults(str = null), Map("str" -> null, "int" -> 42)) testWrite(HasDefaults(str = "dafuq"), Map("str" -> "dafuq", "int" -> 42)) } - test("nested case class with default values") { + test("read case class with default values") { + testRead(Map("str" -> "lol", "int" -> 42), HasDefaults(str = "lol", int = 42)) + testRead(Map("str" -> "lol"), HasDefaults(str = "lol", int = 42)) + testRead(Map("int" -> 43, "str" -> "lol"), HasDefaults(int = 43, str = "lol")) + testRead(Map("str" -> null, "int" -> 42), HasDefaults(str = null, int = 42)) + testRead(Map("str" -> null), HasDefaults(str = null, int = 42)) + testRead(Map(), HasDefaults(str = "dafuq", int = 42)) + } + + test("write case class with opt values") { + testWrite(HasOptParam(str = "lol".opt), Map("flag" -> false, "str" -> "lol")) + testWrite(HasOptParam(), Map("flag" -> false)) + } + + test("write nested case class with default values") { testWrite( value = NestedHasDefaults( flag = false, diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index a186d3aff..534a58703 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -172,24 +172,48 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with } } - def writeField(p: ApplyParam, value: Tree): Tree = { + def doWriteField(p: ApplyParam, value: Tree, transientValue: Option[Tree]): Tree = { + val writeArgs = q"output" :: q"${p.idx}" :: value :: transientValue.toList + val writeTargs = if (isOptimizedPrimitive(p)) Nil else List(p.valueType) + q"writeField[..$writeTargs](..$writeArgs)" + } + + def writeFieldNoTransientDefault(p: ApplyParam, value: Tree): Tree = { + val transientValue = p.optionLike.map(ol => q"${ol.reference(Nil)}.none") + doWriteField(p, value, transientValue) + } + + def writeFieldTransientDefaultPossible(p: ApplyParam, value: Tree): Tree = { val transientValue = if (isTransientDefault(p)) Some(p.defaultValue) else p.optionLike.map(ol => q"${ol.reference(Nil)}.none") - - val writeArgsNoTransient = q"output" :: q"${p.idx}" :: List(value) - val writeArgs = writeArgsNoTransient ::: transientValue.toList - val writeTargs = if (isOptimizedPrimitive(p)) Nil else List(p.valueType) - q""" - if (ignoreTransientDefault) - writeField[..$writeTargs](..$writeArgsNoTransient) - else - writeField[..$writeTargs](..$writeArgs) - """ + doWriteField(p, value, transientValue) } + def writeField(p: ApplyParam, value: Tree, ignoreTransientDefault: Tree): Tree = + if (isTransientDefault(p)) + q""" + if ($ignoreTransientDefault) ${writeFieldNoTransientDefault(p, value)} + else ${writeFieldTransientDefaultPossible(p, value)} + """ + else + writeFieldNoTransientDefault(p, value) + def ignoreTransientDefaultCheck: Tree = - q"val ignoreTransientDefault = output.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + q"output.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + + // when params size is 1 + def writeSingle(p: ApplyParam, value: Tree): Tree = + writeField(p, value, ignoreTransientDefaultCheck) + + // when params size is greater than 1 + def writeMultiple(value: ApplyParam => Tree): Tree = + if (anyParamHasTransientDefault) { + q""" + val ignoreTransientDefault = $ignoreTransientDefaultCheck + ..${params.map(p => writeField(p, value(p), q"ignoreTransientDefault"))} + """ + } else q"..${params.map(p => writeFieldNoTransientDefault(p, value(p)))}" def writeFields: Tree = params match { case Nil => @@ -203,37 +227,30 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with """ case List(p: ApplyParam) => if (canUseFields) - q""" - $ignoreTransientDefaultCheck - ${writeField(p, q"value.${p.sym.name}")} - """ + q"${writeSingle(p, q"value.${p.sym.name}")}" else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) if (unapplyRes.isEmpty) unapplyFailed - else { - $ignoreTransientDefaultCheck - ${writeField(p, q"unapplyRes.get")} - } + else ${writeSingle(p, q"unapplyRes.get")} """ case _ => if (canUseFields) - q""" - $ignoreTransientDefaultCheck - ..${params.map(p => writeField(p, q"value.${p.sym.name}"))} - """ + q"${writeMultiple(p => q"value.${p.sym.name}")}" else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) if (unapplyRes.isEmpty) unapplyFailed else { val t = unapplyRes.get - $ignoreTransientDefaultCheck - ..${params.map(p => writeField(p, q"t.${tupleGet(p.idx)}"))} + ${writeMultiple(p => q"t.${tupleGet(p.idx)}")} } """ } + def anyParamHasTransientDefault: Boolean = + params.exists(isTransientDefault) + def mayBeTransient(p: ApplyParam): Boolean = p.optionLike.nonEmpty || isTransientDefault(p) From 53780f9e805967fe0255e118ca9c0653da4fc147 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 19:49:44 +0200 Subject: [PATCH 6/8] Fix SizedCodec.size marco code when handling IgnoreTransientDefaultMarker --- .../serialization/ObjectSizeTest.scala | 27 +++++++ .../macros/serialization/GenCodecMacros.scala | 76 +++++++++++++------ 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala index 415d706f7..ee2bf3a36 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala @@ -25,10 +25,30 @@ object CustomWrapper extends HasApplyUnapplyCodec[CustomWrapper] { def unapply(cw: CustomWrapper): Opt[String] = Opt(cw.a) } +case class RecordWithOpts( + @optionalParam abc: Opt[String] = Opt.Empty, + @transientDefault flag: Opt[Boolean] = Opt.Empty, + b: Int = 42, +) +object RecordWithOpts extends HasApplyUnapplyCodec[RecordWithOpts] + +case class SingleFieldRecordWithOpts(@optionalParam abc: Opt[String] = Opt.Empty) +object SingleFieldRecordWithOpts extends HasApplyUnapplyCodec[SingleFieldRecordWithOpts] + +case class SingleFieldRecordWithTD(@transientDefault abc: String = "abc") +object SingleFieldRecordWithTD extends HasApplyUnapplyCodec[SingleFieldRecordWithTD] + class ObjectSizeTest extends AnyFunSuite { test("computing object size") { assert(RecordWithDefaults.codec.size(RecordWithDefaults()) == 2) assert(RecordWithDefaults.codec.size(RecordWithDefaults("fuu")) == 3) + assert(RecordWithOpts.codec.size(RecordWithOpts("abc".opt)) == 2) + assert(RecordWithOpts.codec.size(RecordWithOpts("abc".opt, true.opt)) == 3) + assert(RecordWithOpts.codec.size(RecordWithOpts()) == 1) + assert(SingleFieldRecordWithOpts.codec.size(SingleFieldRecordWithOpts()) == 0) + assert(SingleFieldRecordWithOpts.codec.size(SingleFieldRecordWithOpts("abc".opt)) == 1) + assert(SingleFieldRecordWithTD.codec.size(SingleFieldRecordWithTD()) == 0) + assert(SingleFieldRecordWithTD.codec.size(SingleFieldRecordWithTD("haha")) == 1) assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults()) == 1) assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults("fuu")) == 2) assert(CustomWrapper.codec.size(CustomWrapper()) == 0) @@ -46,6 +66,13 @@ class ObjectSizeTest extends AnyFunSuite { } assert(RecordWithDefaults.codec.size(RecordWithDefaults(), defaultIgnoringOutput.opt) == 3) assert(RecordWithDefaults.codec.size(RecordWithDefaults("fuu"), defaultIgnoringOutput.opt) == 3) + assert(RecordWithOpts.codec.size(RecordWithOpts("abc".opt), defaultIgnoringOutput.opt) == 3) + assert(RecordWithOpts.codec.size(RecordWithOpts("abc".opt, true.opt), defaultIgnoringOutput.opt) == 3) + assert(RecordWithOpts.codec.size(RecordWithOpts(), defaultIgnoringOutput.opt) == 2) + assert(SingleFieldRecordWithOpts.codec.size(SingleFieldRecordWithOpts(), defaultIgnoringOutput.opt) == 0) // @optionalParam field should NOT be counted + assert(SingleFieldRecordWithOpts.codec.size(SingleFieldRecordWithOpts("abc".opt), defaultIgnoringOutput.opt) == 1) + assert(SingleFieldRecordWithTD.codec.size(SingleFieldRecordWithTD(), defaultIgnoringOutput.opt) == 1) // @transientDefault field should be counted + assert(SingleFieldRecordWithTD.codec.size(SingleFieldRecordWithTD("haha"), defaultIgnoringOutput.opt) == 1) assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults(), defaultIgnoringOutput.opt) == 2) assert(CustomRecordWithDefaults.codec.size(CustomRecordWithDefaults("fuu"), defaultIgnoringOutput.opt) == 2) } diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index 534a58703..546213530 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -251,38 +251,66 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with def anyParamHasTransientDefault: Boolean = params.exists(isTransientDefault) + def isOptionLike(p: ApplyParam): Boolean = + p.optionLike.nonEmpty + def mayBeTransient(p: ApplyParam): Boolean = - p.optionLike.nonEmpty || isTransientDefault(p) + isOptionLike(p) || isTransientDefault(p) def transientValue(p: ApplyParam): Tree = p.optionLike match { case Some(optionLike) => q"${optionLike.reference(Nil)}.none" case None => p.defaultValue } - def countTransientFields: Tree = + // assumes usage in in size(value, output) method implementation + def countTransientFields: Tree = { + def checkIgnoreTransientDefaultMarker: Tree = + q"output.isDefined && output.get.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + + def doCount(paramsToCount: List[ApplyParam], accessor: ApplyParam => Tree): Tree = + paramsToCount.foldLeft[Tree](q"0") { + (acc, p) => q"$acc + (if(${accessor(p)} == ${transientValue(p)}) 1 else 0)" + } + + def countOnlyOptionLike(accessor: ApplyParam => Tree): Tree = + doCount(params.filter(isOptionLike), accessor) + + def countTransient(accessor: ApplyParam => Tree): Tree = + doCount(params.filter(mayBeTransient), accessor) + + def countMultipleParams(accessor: ApplyParam => Tree): Tree = + if (anyParamHasTransientDefault) + q"if($checkIgnoreTransientDefaultMarker) ${countOnlyOptionLike(accessor)} else ${countTransient(accessor)}" + else + countTransient(accessor) + + def countSingleParam(param: ApplyParam, value: Tree): Tree = + if (isTransientDefault(param)) + q"if(!$checkIgnoreTransientDefaultMarker && $value == ${transientValue(param)}) 1 else 0" + else + q"if($value == ${transientValue(param)}) 1 else 0" + if (canUseFields) - params.filter(mayBeTransient).foldLeft[Tree](q"0") { - (acc, p) => q"$acc + (if(value.${p.sym.name} == ${transientValue(p)}) 1 else 0)" + countMultipleParams(p => q"value.${p.sym.name}") + else if (!params.exists(mayBeTransient)) + q"0" + else + params match { + case List(p: ApplyParam) => + q""" + val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) + if(unapplyRes.isEmpty) unapplyFailed else { ${countSingleParam(p, q"unapplyRes.get")} } + """ + case _ => + q""" + val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) + if(unapplyRes.isEmpty) unapplyFailed else { + val t = unapplyRes.get + ${countMultipleParams(p => q"t.${tupleGet(p.idx)}")} + } + """ } - else if (!params.exists(mayBeTransient)) q"0" - else params match { - case List(p: ApplyParam) => - q""" - val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if(unapplyRes.isEmpty) unapplyFailed else if(unapplyRes.get == ${transientValue(p)}) 1 else 0 - """ - case _ => - val res = params.filter(mayBeTransient).foldLeft[Tree](q"0") { - (acc, p) => q"$acc + (if(t.${tupleGet(p.idx)} == ${transientValue(p)}) 1 else 0)" - } - q""" - val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if(unapplyRes.isEmpty) unapplyFailed else { - val t = unapplyRes.get - $res - } - """ - } + } if (isTransparent(dtpe.typeSymbol)) params match { case List(p: ApplyParam) => @@ -331,7 +359,7 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with val res = q""" def size(value: $dtpe, output: $OptCls[$SerializationPkg.SequentialOutput]): $IntCls = - ${params.size} + ${generated.size} - output.filter(_.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())).mapOr($countTransientFields, _ => 0) + ${params.size} + ${generated.size} - { $countTransientFields } """ List(res) } From e614e2daee4e9a4d5dfdd5e63a4fa0e7b52cf9e4 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 20:10:32 +0200 Subject: [PATCH 7/8] Fix SizedCodec.size for case objects --- .../avsystem/commons/macros/serialization/GenCodecMacros.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index 546213530..5451dd3b9 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -120,7 +120,7 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with q""" new $SerializationPkg.SingletonCodec[$tpe](${tpe.toString}, $safeSingleValue) { ..${generated.map({ case (sym, depTpe) => generatedDepDeclaration(sym, depTpe) })} - override def size(value: $tpe): $IntCls = ${generated.size} + override def size(value: $tpe, output: $OptCls[$SerializationPkg.SequentialOutput]): $IntCls = ${generated.size} override def writeFields(output: $SerializationPkg.ObjectOutput, value: $tpe): $UnitCls = { ..${generated.map({ case (sym, _) => generatedWrite(sym) })} } From c1307e77cdf7a42a7206404522a3e611bbda5ae2 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Wed, 25 Sep 2024 15:49:05 +0200 Subject: [PATCH 8/8] IgnoreTransientDefaultMarker - add comments --- .../serialization/customMarkerWrappers.scala | 41 ++++++++++--------- .../IgnoreTransientDefaultMarkerTest.scala | 6 ++- .../serialization/ObjectSizeTest.scala | 12 +++--- .../cbor/CborInputOutputTest.scala | 14 ++++--- .../macros/serialization/GenCodecMacros.scala | 32 ++++++++------- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala index 4a5e32253..ff3bcc2f6 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/customMarkerWrappers.scala @@ -3,21 +3,18 @@ package serialization trait AcceptsAdditionalCustomMarkers extends AcceptsCustomEvents { - protected def markers: Set[CustomEventMarker[_]] + protected def markers: Set[CustomEventMarker[?]] override def customEvent[T](marker: CustomEventMarker[T], event: T): Boolean = - marker match { - case marker if markers(marker) => true - case _ => super.customEvent(marker, event) - } + markers(marker) || super.customEvent(marker, event) } /** * [[Input]] implementation that adds additional markers [[CustomEventMarker]] to the provided [[Input]] instance */ -final class CustomMarkersInputWrapper( +final class CustomMarkersInputWrapper private( override protected val wrapped: Input, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends InputWrapper with AcceptsAdditionalCustomMarkers { override def readList(): ListInput = @@ -27,19 +24,22 @@ final class CustomMarkersInputWrapper( new CustomMarkersInputWrapper.AdjustedObjectInput(super.readObject(), markers) } object CustomMarkersInputWrapper { - def apply(input: Input, markers: CustomEventMarker[_]*): CustomMarkersInputWrapper = - new CustomMarkersInputWrapper(input, markers.toSet) + def apply(input: Input, markers: CustomEventMarker[?]*): CustomMarkersInputWrapper = + CustomMarkersInputWrapper(input, markers.toSet) + + def apply(input: Input, markers: Set[CustomEventMarker[?]]): CustomMarkersInputWrapper = + new CustomMarkersInputWrapper(input, markers) private final class AdjustedListInput( override protected val wrapped: ListInput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends ListInputWrapper with AcceptsAdditionalCustomMarkers { override def nextElement(): Input = new CustomMarkersInputWrapper(super.nextElement(), markers) } private final class AdjustedFieldInput( override protected val wrapped: FieldInput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends FieldInputWrapper with AcceptsAdditionalCustomMarkers { override def readList(): ListInput = new AdjustedListInput(super.readList(), markers) @@ -48,7 +48,7 @@ object CustomMarkersInputWrapper { private final class AdjustedObjectInput( override protected val wrapped: ObjectInput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends ObjectInputWrapper with AcceptsAdditionalCustomMarkers { override def nextField(): FieldInput = new AdjustedFieldInput(super.nextField(), markers) @@ -60,9 +60,9 @@ object CustomMarkersInputWrapper { /** * [[Output]] implementation that adds additional markers [[CustomEventMarker]] to the provided [[Output]] instance */ -final class CustomMarkersOutputWrapper( +final class CustomMarkersOutputWrapper private( override protected val wrapped: Output, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends OutputWrapper with AcceptsAdditionalCustomMarkers { override def writeSimple(): SimpleOutput = @@ -76,17 +76,20 @@ final class CustomMarkersOutputWrapper( } object CustomMarkersOutputWrapper { - def apply(output: Output, markers: CustomEventMarker[_]*): CustomMarkersOutputWrapper = - new CustomMarkersOutputWrapper(output, markers.toSet) + def apply(output: Output, markers: CustomEventMarker[?]*): CustomMarkersOutputWrapper = + CustomMarkersOutputWrapper(output, markers.toSet) + + def apply(output: Output, markers: Set[CustomEventMarker[?]]): CustomMarkersOutputWrapper = + new CustomMarkersOutputWrapper(output, markers) private final class AdjustedSimpleOutput( override protected val wrapped: SimpleOutput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends SimpleOutputWrapper with AcceptsAdditionalCustomMarkers private final class AdjustedListOutput( override protected val wrapped: ListOutput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends ListOutputWrapper with AcceptsAdditionalCustomMarkers { override def writeElement(): Output = @@ -95,7 +98,7 @@ object CustomMarkersOutputWrapper { private final class AdjustedObjectOutput( override protected val wrapped: ObjectOutput, - override protected val markers: Set[CustomEventMarker[_]], + override protected val markers: Set[CustomEventMarker[?]], ) extends ObjectOutputWrapper with AcceptsAdditionalCustomMarkers { override def writeField(key: String): Output = diff --git a/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala index 30fe2c0b2..b2f6b9aef 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/IgnoreTransientDefaultMarkerTest.scala @@ -20,13 +20,13 @@ object IgnoreTransientDefaultMarkerTest { } class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { - import IgnoreTransientDefaultMarkerTest._ + import IgnoreTransientDefaultMarkerTest.* override type Raw = Any def writeToOutput(write: Output => Unit): Any = { var result: Any = null - write(CustomMarkersOutputWrapper(new SimpleValueOutput(result = _), IgnoreTransientDefaultMarker)) + write(CustomMarkersOutputWrapper(new SimpleValueOutput(v => result = v), IgnoreTransientDefaultMarker)) result } @@ -40,6 +40,7 @@ class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { testWrite(HasDefaults(str = "dafuq"), Map("str" -> "dafuq", "int" -> 42)) } + //noinspection RedundantDefaultArgument test("read case class with default values") { testRead(Map("str" -> "lol", "int" -> 42), HasDefaults(str = "lol", int = 42)) testRead(Map("str" -> "lol"), HasDefaults(str = "lol", int = 42)) @@ -54,6 +55,7 @@ class IgnoreTransientDefaultMarkerTest extends AbstractCodecTest { testWrite(HasOptParam(), Map("flag" -> false)) } + //noinspection RedundantDefaultArgument test("write nested case class with default values") { testWrite( value = NestedHasDefaults( diff --git a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala index ee2bf3a36..35fd3b16c 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/ObjectSizeTest.scala @@ -3,7 +3,7 @@ package serialization import org.scalatest.funsuite.AnyFunSuite -case class RecordWithDefaults( +final case class RecordWithDefaults( @transientDefault a: String = "", b: Int = 42 ) { @@ -11,7 +11,7 @@ case class RecordWithDefaults( } object RecordWithDefaults extends HasApplyUnapplyCodec[RecordWithDefaults] -class CustomRecordWithDefaults(val a: String, val b: Int) +final class CustomRecordWithDefaults(val a: String, val b: Int) object CustomRecordWithDefaults extends HasApplyUnapplyCodec[CustomRecordWithDefaults] { def apply(@transientDefault a: String = "", b: Int = 42): CustomRecordWithDefaults = new CustomRecordWithDefaults(a, b) @@ -19,23 +19,23 @@ object CustomRecordWithDefaults extends HasApplyUnapplyCodec[CustomRecordWithDef Opt((crwd.a, crwd.b)) } -class CustomWrapper(val a: String) +final class CustomWrapper(val a: String) object CustomWrapper extends HasApplyUnapplyCodec[CustomWrapper] { def apply(@transientDefault a: String = ""): CustomWrapper = new CustomWrapper(a) def unapply(cw: CustomWrapper): Opt[String] = Opt(cw.a) } -case class RecordWithOpts( +final case class RecordWithOpts( @optionalParam abc: Opt[String] = Opt.Empty, @transientDefault flag: Opt[Boolean] = Opt.Empty, b: Int = 42, ) object RecordWithOpts extends HasApplyUnapplyCodec[RecordWithOpts] -case class SingleFieldRecordWithOpts(@optionalParam abc: Opt[String] = Opt.Empty) +final case class SingleFieldRecordWithOpts(@optionalParam abc: Opt[String] = Opt.Empty) object SingleFieldRecordWithOpts extends HasApplyUnapplyCodec[SingleFieldRecordWithOpts] -case class SingleFieldRecordWithTD(@transientDefault abc: String = "abc") +final case class SingleFieldRecordWithTD(@transientDefault abc: String = "abc") object SingleFieldRecordWithTD extends HasApplyUnapplyCodec[SingleFieldRecordWithTD] class ObjectSizeTest extends AnyFunSuite { diff --git a/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala index dbc4500fe..96798f1fb 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/cbor/CborInputOutputTest.scala @@ -10,7 +10,7 @@ import org.scalatest.funsuite.AnyFunSuite import java.io.{ByteArrayOutputStream, DataOutputStream} -case class Record( +final case class Record( b: Boolean, i: Int, l: List[String], @@ -19,7 +19,7 @@ case class Record( ) object Record extends HasGenCodec[Record] -case class CustomKeysRecord( +final case class CustomKeysRecord( @cborKey(1) first: Int, @cborKey(true) second: Boolean, @cborKey(Vector(1, 2, 3)) third: String, @@ -28,13 +28,13 @@ case class CustomKeysRecord( ) object CustomKeysRecord extends HasCborCodec[CustomKeysRecord] -case class CustomKeysRecordWithDefaults( +final case class CustomKeysRecordWithDefaults( @transientDefault @cborKey(1) first: Int = 0, @cborKey(true) second: Boolean, ) object CustomKeysRecordWithDefaults extends HasCborCodec[CustomKeysRecordWithDefaults] -case class CustomKeysRecordWithNoDefaults( +final case class CustomKeysRecordWithNoDefaults( @cborKey(1) first: Int = 0, @cborKey(true) second: Boolean, ) @@ -242,11 +242,13 @@ class CborInputOutputTest extends AnyFunSuite { val value = CustomKeysRecordWithDefaults(first = 0, second = true) GenCodec.write(output, value) val bytes = Bytes(baos.toByteArray) - assert(bytes.toString == "A20100F5F5") + + val expectedRawValue = "A20100F5F5" + assert(bytes.toString == expectedRawValue) assert(RawCbor(bytes.bytes).readAs[CustomKeysRecordWithDefaults](keyCodec) == value) // should be the same as model with @transientDefault and serialization ignoring it - assertRoundtrip(CustomKeysRecordWithNoDefaults(first = 0, second = true), "A20100F5F5") + assertRoundtrip(CustomKeysRecordWithNoDefaults(first = 0, second = true), expectedRawValue) } test("chunked text string") { diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index 5451dd3b9..36a92e5bb 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -10,6 +10,8 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with import c.universe._ + private def IgnoreTransientDefaultMarkerObj: Tree = q"$SerializationPkg.IgnoreTransientDefaultMarker" + override def allowOptionalParams: Boolean = true def mkTupleCodec[T: WeakTypeTag](elementCodecs: Tree*): Tree = instrument { @@ -183,24 +185,21 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with doWriteField(p, value, transientValue) } - def writeFieldTransientDefaultPossible(p: ApplyParam, value: Tree): Tree = { - val transientValue = - if (isTransientDefault(p)) Some(p.defaultValue) - else p.optionLike.map(ol => q"${ol.reference(Nil)}.none") - doWriteField(p, value, transientValue) - } + def writeFieldTransientDefaultPossible(p: ApplyParam, value: Tree): Tree = + if (isTransientDefault(p)) doWriteField(p, value, Some(p.defaultValue)) + else writeFieldNoTransientDefault(p, value) def writeField(p: ApplyParam, value: Tree, ignoreTransientDefault: Tree): Tree = - if (isTransientDefault(p)) + if (isTransientDefault(p)) // optimize code to avoid calling 'output.customEvent' when param does not have @transientDefault q""" - if ($ignoreTransientDefault) ${writeFieldNoTransientDefault(p, value)} + if($ignoreTransientDefault) ${writeFieldNoTransientDefault(p, value)} else ${writeFieldTransientDefaultPossible(p, value)} """ else writeFieldNoTransientDefault(p, value) def ignoreTransientDefaultCheck: Tree = - q"output.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + q"output.customEvent($IgnoreTransientDefaultMarkerObj, ())" // when params size is 1 def writeSingle(p: ApplyParam, value: Tree): Tree = @@ -208,12 +207,15 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with // when params size is greater than 1 def writeMultiple(value: ApplyParam => Tree): Tree = - if (anyParamHasTransientDefault) { + // optimize code to avoid calling 'output.customEvent' when there no params with @transientDefault + // extracted to `val` to avoid calling 'output.customEvent' multiple times + if (anyParamHasTransientDefault) q""" val ignoreTransientDefault = $ignoreTransientDefaultCheck ..${params.map(p => writeField(p, value(p), q"ignoreTransientDefault"))} """ - } else q"..${params.map(p => writeFieldNoTransientDefault(p, value(p)))}" + else + q"..${params.map(p => writeFieldNoTransientDefault(p, value(p)))}" def writeFields: Tree = params match { case Nil => @@ -231,7 +233,7 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if (unapplyRes.isEmpty) unapplyFailed + if(unapplyRes.isEmpty) unapplyFailed else ${writeSingle(p, q"unapplyRes.get")} """ case _ => @@ -240,7 +242,7 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with else q""" val unapplyRes = $companion.$unapply[..${dtpe.typeArgs}](value) - if (unapplyRes.isEmpty) unapplyFailed + if(unapplyRes.isEmpty) unapplyFailed else { val t = unapplyRes.get ${writeMultiple(p => q"t.${tupleGet(p.idx)}")} @@ -262,10 +264,10 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with case None => p.defaultValue } - // assumes usage in in size(value, output) method implementation + // assumes usage in SizedCodec.size(value, output) method implementation def countTransientFields: Tree = { def checkIgnoreTransientDefaultMarker: Tree = - q"output.isDefined && output.get.customEvent($SerializationPkg.IgnoreTransientDefaultMarker, ())" + q"output.isDefined && output.get.customEvent($IgnoreTransientDefaultMarkerObj, ())" def doCount(paramsToCount: List[ApplyParam], accessor: ApplyParam => Tree): Tree = paramsToCount.foldLeft[Tree](q"0") {