Skip to content

Commit a342a55

Browse files
authored
Merge pull request #52 from AVSystem/generalizations
Relaxed field order preservation requirement for ObjectInput
2 parents b1e6575 + 068054e commit a342a55

File tree

6 files changed

+174
-93
lines changed

6 files changed

+174
-93
lines changed

commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala

+11
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs {
142142
def createNonNullList[T](readFun: ListInput => T, writeFun: (ListOutput, T) => Any) =
143143
createList(readFun, writeFun, allowNull = false)
144144

145+
/**
146+
* Helper method to manually implement a `GenCodec` that writes an object. NOTE: in most cases the easiest way to
147+
* have a custom object codec is to manually implement `apply` and `unapply`/`unapplySeq` methods in companion object
148+
* of your type or use [[fromApplyUnapplyProvider]] if the type comes from a third party code and you can't
149+
* modify its companion object.
150+
*/
145151
def createObject[T](readFun: ObjectInput => T, writeFun: (ObjectOutput, T) => Any, allowNull: Boolean) =
146152
new ObjectCodec[T] {
147153
def nullable = allowNull
@@ -214,6 +220,11 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs {
214220
}
215221
}
216222

223+
/**
224+
* Convenience base class for `GenCodec`s that serialize values as objects.
225+
* NOTE: if you need to implement a custom `GenCodec` that writes an object, the best way to do it is to have
226+
* manually implemented `apply` and `unapply` in companion object or by using [[GenCodec.fromApplyUnapplyProvider]].
227+
*/
217228
trait ObjectCodec[T] extends NullSafeCodec[T] {
218229
def readObject(input: ObjectInput): T
219230
def writeObject(output: ObjectOutput, value: T): Unit

commons-core/src/main/scala/com/avsystem/commons/serialization/InputOutput.scala

+26-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.avsystem.commons
22
package serialization
33

4-
import com.avsystem.commons.serialization.GenCodec.ReadFailure
5-
64
/**
75
* Represents an abstract sink to which a value may be serialized (written).
86
* An [[Output]] instance should be assumed to be stateful. After calling any of the `write` methods, it MUST NOT be
@@ -192,12 +190,35 @@ trait ObjectInput extends Any with SequentialInput { self =>
192190
* You MUST NOT call `nextField()` again until this [[com.avsystem.commons.serialization.FieldInput FieldInput]]
193191
* is fully read or skipped.
194192
* </p>
195-
* Subsequent invocations of `nextField` MUST return fields in exactly the same order as they were written
196-
* using [[com.avsystem.commons.serialization.ObjectOutput.writeField ObjectOutput.writeField]].
197-
* In other words, serialization format MUST preserve order of object fields.
193+
* Serialization format implemented by this `ObjectInput` must either preserve order of fields (as they are
194+
* written by corresponding `ObjectOutput`) OR it must provide random field access capability.
195+
* <ul>
196+
* <li>If the serialization format is able to preserve object field order then [[nextField]] must return
197+
* object fields in exactly the same order as they were written by `ObjectOutput.writeField`. This is
198+
* natural for most serialization formats backed by strings, raw character or byte sequences, e.g.
199+
* JSON implemented by [[com.avsystem.commons.serialization.json.JsonStringOutput JsonStringOutput]]/
200+
* [[com.avsystem.commons.serialization.json.JsonStringInput JsonStringInput]].</li>
201+
* <li>If the serialization format is unable to preserve object field order (e.g. because it uses hash maps to
202+
* represent objects) then it must instead support random, by-name field access by overriding [[peekField]].
203+
* </li>
204+
* </ul>
198205
*/
199206
def nextField(): FieldInput
200207

208+
/**
209+
* If serialization format implemented by `ObjectInput` does NOT preserve field order, then this method MUST
210+
* be overridden to support random field access. It should return non-empty [[Opt]] containing input for every field
211+
* present in the object, regardless of field order assumed by [[nextField]].
212+
* `Opt.Empty` is returned when field is absent or always when this `ObjectInput` does not support random field
213+
* access (in which case it must preserve field order instead).
214+
* NOTE: calling [[peekField]] and using [[FieldInput]] returned by it MUST NOT change state of this `ObjectInput`.
215+
* Therefore, it cannot in any way influence results returned by [[nextField]] and [[hasNext]].
216+
* For example, if a [[FieldInput]] for particular field has already been
217+
* accessed using [[peekField]] but has not yet been returned by [[nextField]] then it MUST be returned at some
218+
* point in the future by [[nextField]].
219+
*/
220+
def peekField(name: String): Opt[FieldInput] = Opt.Empty
221+
201222
def skipRemaining() = while (hasNext) nextField().skip()
202223
def iterator[A](readFun: Input => A): Iterator[(String, A)] =
203224
new Iterator[(String, A)] {
@@ -214,11 +235,4 @@ trait ObjectInput extends Any with SequentialInput { self =>
214235
*/
215236
trait FieldInput extends Input {
216237
def fieldName: String
217-
218-
def assertField(expectedName: String): this.type = {
219-
if (fieldName != expectedName) {
220-
throw new ReadFailure(s"Expected $expectedName as next field, got $fieldName")
221-
}
222-
this
223-
}
224238
}

commons-core/src/main/scala/com/avsystem/commons/serialization/SimpleValueInputOutput.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class SimpleValueOutput(consumer: Any => Unit) extends Output {
5555
def writeBoolean(boolean: Boolean) = consumer(boolean)
5656

5757
def writeObject() = new ObjectOutput {
58-
private val result = new mutable.LinkedHashMap[String, Any]
58+
private val result = new mutable.HashMap[String, Any]
5959
def writeField(key: String) = new SimpleValueOutput(v => result += ((key, v)))
6060
def finish() = consumer(result)
6161
}
@@ -95,10 +95,12 @@ class SimpleValueInput(value: Any) extends Input {
9595
def readNull() = if (value == null) null else throw new ReadFailure("not null")
9696
def readObject() =
9797
new ObjectInput {
98-
private val it = doRead[BMap[String, Any]].iterator.map {
98+
private val map = doRead[BMap[String, Any]]
99+
private val it = map.iterator.map {
99100
case (k, v) => new SimpleValueFieldInput(k, v)
100101
}
101102
def nextField() = it.next()
103+
override def peekField(name: String) = map.getOpt(name).map(new SimpleValueFieldInput(name, _))
102104
def hasNext = it.hasNext
103105
}
104106

commons-core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala

+14-8
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,19 @@ abstract class FlatSealedHierarchyCodec[T](
143143
final def readObject(input: ObjectInput): T = {
144144
val oooFields = new FieldValues(oooFieldNames, oooDeps, typeRepr)
145145

146+
def readCase(caseNameField: FieldInput): T = {
147+
val caseName = readCaseName(caseNameField)
148+
caseIndexByName(caseName) match {
149+
case -1 => unknownCase(caseName)
150+
case idx => readFlatCase(caseName, oooFields, input, caseDeps(idx))
151+
}
152+
}
153+
146154
def read(): T =
147155
if (input.hasNext) {
148156
val fi = input.nextField()
149-
if (fi.fieldName == caseFieldName) {
150-
val caseName = readCaseName(fi)
151-
caseIndexByName(caseName) match {
152-
case -1 => unknownCase(caseName)
153-
case idx => readFlatCase(caseName, oooFields, input, caseDeps(idx))
154-
}
155-
} else if (!oooFields.tryReadField(fi)) {
157+
if (fi.fieldName == caseFieldName) readCase(fi)
158+
else if (!oooFields.tryReadField(fi)) {
156159
if (caseDependentFieldNames.contains(fi.fieldName)) {
157160
if (defaultCaseIdx != -1) {
158161
val defaultCaseName = caseNames(defaultCaseIdx)
@@ -174,7 +177,10 @@ abstract class FlatSealedHierarchyCodec[T](
174177
missingCase
175178
}
176179

177-
read()
180+
input.peekField(caseFieldName) match {
181+
case Opt(fi) => readCase(fi)
182+
case Opt.Empty => read()
183+
}
178184
}
179185
}
180186

commons-core/src/test/scala/com/avsystem/commons/serialization/GenCodecTest.scala

+48-42
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ class GenCodecTest extends CodecTestBase {
7070
testWriteReadAndAutoWriteRead[JSortedSet[Int]](jTreeSet, List(1, 2, 3))
7171
testWriteReadAndAutoWriteRead[JNavigableSet[Int]](jTreeSet, List(1, 2, 3))
7272
testWriteReadAndAutoWriteRead[JTreeSet[Int]](jTreeSet, List(1, 2, 3))
73-
testWriteReadAndAutoWriteRead[JMap[String, Int]](jHashMap, ListMap("1" -> 1, "2" -> 2, "3" -> 3))
74-
testWriteReadAndAutoWriteRead[JHashMap[String, Int]](jHashMap, ListMap("1" -> 1, "2" -> 2, "3" -> 3))
75-
testWriteReadAndAutoWriteRead[JLinkedHashMap[String, Int]](jLinkedHashMap, ListMap("1" -> 1, "2" -> 2, "3" -> 3))
76-
testWriteReadAndAutoWriteRead[JHashMap[Int, Int]](jIntHashMap, ListMap("1" -> 1, "2" -> 2, "3" -> 3))
73+
testWriteReadAndAutoWriteRead[JMap[String, Int]](jHashMap, Map("1" -> 1, "2" -> 2, "3" -> 3))
74+
testWriteReadAndAutoWriteRead[JHashMap[String, Int]](jHashMap, Map("1" -> 1, "2" -> 2, "3" -> 3))
75+
testWriteReadAndAutoWriteRead[JLinkedHashMap[String, Int]](jLinkedHashMap, Map("1" -> 1, "2" -> 2, "3" -> 3))
76+
testWriteReadAndAutoWriteRead[JHashMap[Int, Int]](jIntHashMap, Map("1" -> 1, "2" -> 2, "3" -> 3))
7777
}
7878

7979
test("NoState test") {
@@ -83,12 +83,12 @@ class GenCodecTest extends CodecTestBase {
8383

8484
test("collections and wrappers test") {
8585
testWriteReadAndAutoWriteRead[Option[Int]](option, List(42))
86-
testWriteReadAndAutoWriteRead[Either[Int, String]](Left(42), ListMap("Left" -> 42))
87-
testWriteReadAndAutoWriteRead[Either[Int, String]](Right("lol"), ListMap("Right" -> "lol"))
86+
testWriteReadAndAutoWriteRead[Either[Int, String]](Left(42), Map("Left" -> 42))
87+
testWriteReadAndAutoWriteRead[Either[Int, String]](Right("lol"), Map("Right" -> "lol"))
8888
testWriteReadAndAutoWriteRead[List[Int]](list, list)
8989
testWriteReadAndAutoWriteRead[Set[Int]](set, set.toList)
9090
testWriteReadAndAutoWriteRead[Map[String, Int]](map, map)
91-
testWriteReadAndAutoWriteRead[Map[Int, Int]](intMap, ListMap("1" -> 1, "2" -> 2, "3" -> 3))
91+
testWriteReadAndAutoWriteRead[Map[Int, Int]](intMap, Map("1" -> 1, "2" -> 2, "3" -> 3))
9292
testWriteReadAndAutoWriteRead[IHashMap[String, Int]](hashMap, hashMap)
9393
}
9494

@@ -122,7 +122,7 @@ class GenCodecTest extends CodecTestBase {
122122
object SingleArgCaseClass extends HasGenCodec[SingleArgCaseClass]
123123

124124
test("single arg case class test") {
125-
testWriteReadAndAutoWriteRead(SingleArgCaseClass("something"), ListMap("str" -> "something"))
125+
testWriteReadAndAutoWriteRead(SingleArgCaseClass("something"), Map("str" -> "something"))
126126
}
127127

128128
@transparent
@@ -149,7 +149,7 @@ class GenCodecTest extends CodecTestBase {
149149

150150
test("case class test") {
151151
testWriteReadAndAutoWriteRead(SomeCaseClass("dafuq", List(1, 2, 3)),
152-
ListMap("some.str" -> "dafuq", "intList" -> List(1, 2, 3), "someStrLen" -> 5)
152+
Map("some.str" -> "dafuq", "intList" -> List(1, 2, 3), "someStrLen" -> 5)
153153
)
154154
}
155155

@@ -166,7 +166,7 @@ class GenCodecTest extends CodecTestBase {
166166
}
167167

168168
test("case class with wildcard test") {
169-
testWriteReadAndAutoWriteRead(CaseClassWithWildcard(Stuff("lol")), ListMap("stuff" -> "lol"))
169+
testWriteReadAndAutoWriteRead(CaseClassWithWildcard(Stuff("lol")), Map("stuff" -> "lol"))
170170
}
171171

172172
class CaseClassLike(val str: String, val intList: List[Int])
@@ -179,7 +179,7 @@ class GenCodecTest extends CodecTestBase {
179179

180180
test("case class like test") {
181181
testWriteReadAndAutoWriteRead(CaseClassLike("dafuq", List(1, 2, 3)),
182-
ListMap("some.str" -> "dafuq", "intList" -> List(1, 2, 3))
182+
Map("some.str" -> "dafuq", "intList" -> List(1, 2, 3))
183183
)
184184
}
185185

@@ -199,7 +199,7 @@ class GenCodecTest extends CodecTestBase {
199199

200200
test("case class like with inherited apply/unapply test") {
201201
testWriteReadAndAutoWriteRead(HasInheritedApply("dafuq", List(1, 2, 3)),
202-
ListMap("a" -> "dafuq", "lb" -> List(1, 2, 3))
202+
Map("a" -> "dafuq", "lb" -> List(1, 2, 3))
203203
)
204204
}
205205

@@ -212,7 +212,7 @@ class GenCodecTest extends CodecTestBase {
212212
test("apply/unapply provider based codec test") {
213213
implicit val tpCodec: GenCodec[ThirdParty] = GenCodec.fromApplyUnapplyProvider[ThirdParty](ThirdPartyFakeCompanion)
214214
testWriteReadAndAutoWriteRead(ThirdParty(42, "lol"),
215-
ListMap("str" -> "lol", "int" -> 42)
215+
Map("str" -> "lol", "int" -> 42)
216216
)
217217
}
218218

@@ -223,7 +223,7 @@ class GenCodecTest extends CodecTestBase {
223223

224224
test("varargs case class test") {
225225
testWriteReadAndAutoWriteRead(VarargsCaseClass(42, "foo", "bar"),
226-
ListMap("int" -> 42, "strings" -> List("foo", "bar"))
226+
Map("int" -> 42, "strings" -> List("foo", "bar"))
227227
)
228228
}
229229

@@ -237,7 +237,7 @@ class GenCodecTest extends CodecTestBase {
237237

238238
test("varargs case class like test") {
239239
testWriteReadAndAutoWriteRead(VarargsCaseClassLike("dafuq", 1, 2, 3),
240-
ListMap("some.str" -> "dafuq", "ints" -> List(1, 2, 3))
240+
Map("some.str" -> "dafuq", "ints" -> List(1, 2, 3))
241241
)
242242
}
243243

@@ -247,9 +247,9 @@ class GenCodecTest extends CodecTestBase {
247247
}
248248

249249
test("case class with default values test") {
250-
testWriteReadAndAutoWriteRead(HasDefaults(str = "lol"), ListMap("str" -> "lol"))
251-
testWriteReadAndAutoWriteRead(HasDefaults(43, "lol"), ListMap("int" -> 43, "str" -> "lol"))
252-
testWriteReadAndAutoWriteRead(HasDefaults(str = null), ListMap("str" -> null))
250+
testWriteReadAndAutoWriteRead(HasDefaults(str = "lol"), Map("str" -> "lol"))
251+
testWriteReadAndAutoWriteRead(HasDefaults(43, "lol"), Map("int" -> 43, "str" -> "lol"))
252+
testWriteReadAndAutoWriteRead(HasDefaults(str = null), Map("str" -> null))
253253
}
254254

255255
case class Node[T](value: T, children: List[Node[T]] = Nil)
@@ -287,41 +287,47 @@ class GenCodecTest extends CodecTestBase {
287287
}
288288

289289
test("recursively defined sealed hierarchy with explicit case class codec test") {
290-
testWriteReadAndAutoWriteRead[CustomList](CustomTail, ListMap("CustomTail" -> Map()))
290+
testWriteReadAndAutoWriteRead[CustomList](CustomTail, Map("CustomTail" -> Map()))
291291
testWriteReadAndAutoWriteRead[CustomList](CustomCons(CustomCons(CustomTail)),
292-
ListMap("CustomCons" -> ListMap("CustomCons" -> ListMap("CustomTail" -> Map()))))
292+
Map("CustomCons" -> Map("CustomCons" -> Map("CustomTail" -> Map()))))
293293
}
294294

295295
test("value class test") {
296-
testWriteReadAndAutoWriteRead(ValueClass("costam"), ListMap("str" -> "costam"))
296+
testWriteReadAndAutoWriteRead(ValueClass("costam"), Map("str" -> "costam"))
297297
}
298298

299299
test("sealed hierarchy test") {
300300
testWriteReadAndAutoWriteRead[SealedBase](SealedBase.CaseObject,
301-
ListMap("CaseObject" -> Map()))
301+
Map("CaseObject" -> Map()))
302302
testWriteReadAndAutoWriteRead[SealedBase](SealedBase.CaseClass("fuu"),
303-
ListMap("CaseClass" -> ListMap("str" -> "fuu")))
303+
Map("CaseClass" -> Map("str" -> "fuu")))
304304
testWriteReadAndAutoWriteRead[SealedBase](SealedBase.InnerBase.InnerCaseObject,
305-
ListMap("InnerCaseObject" -> Map()))
305+
Map("InnerCaseObject" -> Map()))
306306
testWriteReadAndAutoWriteRead[SealedBase](SealedBase.InnerBase.InnerCaseClass("fuu"),
307-
ListMap("InnerCaseClass" -> ListMap("str" -> "fuu")))
307+
Map("InnerCaseClass" -> Map("str" -> "fuu")))
308308
}
309309

310310
test("flat sealed hierarchy test") {
311311
testWriteReadAndAutoWriteRead[FlatSealedBase](FlatSealedBase.FirstCase("fuu", 42),
312-
ListMap("_case" -> "FirstCase", "_id" -> "fuu", "int" -> 42, "upper_id" -> "FUU"))
312+
Map("_case" -> "FirstCase", "_id" -> "fuu", "int" -> 42, "upper_id" -> "FUU"))
313313
testWriteReadAndAutoWriteRead[FlatSealedBase](FlatSealedBase.SecondCase("bar", 3.14, 1.0, 2.0),
314-
ListMap("_case" -> "SecondCase", "_id" -> "bar", "dbl" -> 3.14, "moar" -> List(1.0, 2.0), "upper_id" -> "BAR"))
314+
Map("_case" -> "SecondCase", "_id" -> "bar", "dbl" -> 3.14, "moar" -> List(1.0, 2.0), "upper_id" -> "BAR"))
315315
testWriteReadAndAutoWriteRead[FlatSealedBase](FlatSealedBase.ThirdCase,
316-
ListMap("_case" -> "ThirdCase", "_id" -> "third", "upper_id" -> "THIRD"))
316+
Map("_case" -> "ThirdCase", "_id" -> "third", "upper_id" -> "THIRD"))
317+
}
318+
319+
test("random field access dependent flat sealed hierarchy reading test") {
320+
testReadAndAutoRead[FlatSealedBase](
321+
ListMap("_id" -> "fuu", "int" -> 42, "upper_id" -> "FUU", "_case" -> "FirstCase"),
322+
FlatSealedBase.FirstCase("fuu", 42))
317323
}
318324

319325
test("out of order field in flat sealed hierarchy test") {
320326
testReadAndAutoRead[FlatSealedBase](
321-
ListMap("_id" -> "fuu", "upper_id" -> "FUU", "random" -> 13, "_case" -> "FirstCase", "int" -> 42),
327+
Map("_id" -> "fuu", "upper_id" -> "FUU", "random" -> 13, "_case" -> "FirstCase", "int" -> 42),
322328
FlatSealedBase.FirstCase("fuu", 42))
323329
testReadAndAutoRead[FlatSealedBase](
324-
ListMap("_id" -> "bar", "upper_id" -> "FUU", "random" -> 13, "_case" -> "SecondCase", "dbl" -> 3.14, "moar" -> List(1.0, 2.0)),
330+
Map("_id" -> "bar", "upper_id" -> "FUU", "random" -> 13, "_case" -> "SecondCase", "dbl" -> 3.14, "moar" -> List(1.0, 2.0)),
325331
FlatSealedBase.SecondCase("bar", 3.14, 1.0, 2.0))
326332
}
327333

@@ -342,10 +348,10 @@ class GenCodecTest extends CodecTestBase {
342348
}
343349

344350
test("GADT test") {
345-
testWriteReadAndAutoWriteRead[Expr[_]](NullExpr, ListMap("NullExpr" -> Map()))
346-
testWriteReadAndAutoWriteRead[Expr[_]](StringExpr("stringzor"), ListMap("StringExpr" -> ListMap("str" -> "stringzor")))
347-
testWriteReadAndAutoWriteRead[Expr[String]](StringExpr("stringzor"), ListMap("StringExpr" -> ListMap("str" -> "stringzor")))
348-
testWriteReadAndAutoWriteRead[BaseExpr](StringExpr("stringzor"), ListMap("StringExpr" -> ListMap("str" -> "stringzor")))
351+
testWriteReadAndAutoWriteRead[Expr[_]](NullExpr, Map("NullExpr" -> Map()))
352+
testWriteReadAndAutoWriteRead[Expr[_]](StringExpr("stringzor"), Map("StringExpr" -> Map("str" -> "stringzor")))
353+
testWriteReadAndAutoWriteRead[Expr[String]](StringExpr("stringzor"), Map("StringExpr" -> Map("str" -> "stringzor")))
354+
testWriteReadAndAutoWriteRead[BaseExpr](StringExpr("stringzor"), Map("StringExpr" -> Map("str" -> "stringzor")))
349355
}
350356

351357
sealed trait Tree[T]
@@ -370,11 +376,11 @@ class GenCodecTest extends CodecTestBase {
370376
Leaf(3)
371377
)
372378
),
373-
ListMap("Branch" -> Map(
374-
"left" -> ListMap("Leaf" -> ListMap("value" -> 1)),
375-
"right" -> ListMap("Branch" -> Map(
376-
"left" -> ListMap("Leaf" -> ListMap("value" -> 2)),
377-
"right" -> ListMap("Leaf" -> ListMap("value" -> 3))
379+
Map("Branch" -> Map(
380+
"left" -> Map("Leaf" -> Map("value" -> 1)),
381+
"right" -> Map("Branch" -> Map(
382+
"left" -> Map("Leaf" -> Map("value" -> 2)),
383+
"right" -> Map("Leaf" -> Map("value" -> 3))
378384
))
379385
))
380386
)
@@ -391,9 +397,9 @@ class GenCodecTest extends CodecTestBase {
391397
}
392398

393399
test("sealed enum test") {
394-
testWriteReadAndAutoWriteRead[Enumz](Enumz.First, ListMap("Primary" -> Map()))
395-
testWriteReadAndAutoWriteRead[Enumz](Enumz.Second, ListMap("Second" -> Map()))
396-
testWriteReadAndAutoWriteRead[Enumz](Enumz.Third, ListMap("Third" -> Map()))
400+
testWriteReadAndAutoWriteRead[Enumz](Enumz.First, Map("Primary" -> Map()))
401+
testWriteReadAndAutoWriteRead[Enumz](Enumz.Second, Map("Second" -> Map()))
402+
testWriteReadAndAutoWriteRead[Enumz](Enumz.Third, Map("Third" -> Map()))
397403
}
398404

399405
sealed trait KeyEnumz

0 commit comments

Comments
 (0)