├── .gitignore ├── .jvmopts ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark └── src │ ├── main │ └── scala │ │ └── com │ │ └── evolutiongaming │ │ └── kryo │ │ └── SerializerBenchmark.scala │ └── test │ └── scala │ └── com │ └── evolutiongaming │ └── kryo │ └── SerializerBenchmarkSpec.scala ├── build.sbt ├── macros └── src │ ├── main │ └── scala │ │ └── com │ │ └── evolutiongaming │ │ └── kryo │ │ ├── ConstSerializer.scala │ │ ├── Empty.scala │ │ └── Serializer.scala │ └── test │ ├── java │ └── com │ │ └── evolutiongaming │ │ └── kryo │ │ └── Suit.java │ └── scala │ └── com │ └── evolutiongaming │ └── kryo │ └── SerializerMacroSpec.scala ├── project ├── build.properties └── plugins.sbt ├── release.sbt └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | # Idea 20 | .idea/ 21 | *.iml 22 | *.ipr 23 | 24 | # Mac 25 | .DS_Store -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xmx4g 2 | -XX:MaxMetaspaceSize=512m 3 | -XX:ReservedCodeCacheSize=256m 4 | -XX:+UseParallelGC 5 | -Dfile.encoding=UTF8 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # by default Travis uses JDK 8u31, which is much too old for Scala 2.12 (versions before 8u102 have issues - https://issues.scala-lang.org/browse/SI-9828) 2 | dist: trusty 3 | sudo: false 4 | 5 | language: scala 6 | 7 | scala: 8 | - 2.12.8 9 | - 2.13.0 10 | 11 | jdk: oraclejdk8 12 | 13 | script: sbt ++$TRAVIS_SCALA_VERSION clean coverage test 14 | 15 | after_success: sbt ++$TRAVIS_SCALA_VERSION coverageReport coveralls 16 | 17 | notifications: 18 | slack: 19 | on_success: never 20 | on_failure: always 21 | rooms: 22 | secure: "BKtsWdL49PlO6zwuQms/Z2BKziNY0PAbcVtXHLAkIgOEs78d8+3lisHyYpF48sg8uXyXFJqL4SLeUD8AECwOabG/7wODxELqEdjg6YKVNVtyBpW7vQFZ/+G4WCJjz818I2ncxdhzGdnXlYIEAxTKCIEVVL7cTk1fX5lgWCWJ6rZQXpp2+hg0KuMNtzZM0dYakAig3Y6lx4xG0lqB7fX35LP12573uQWYO+iXBZRpmJ5YRvgFm6vkvLv6NW0lbEVfQjFoKmfIkQFD1B4Ao/cf+ee8DuwppGtR+qbvgyR7DMl6yZX26UJJ3XInMi+cGzfCIIrNIW5oTb9y5VBkxbbJEHQmrw31QkDzBwDPgqzMjKLQSsE9jbENLkDaQ02ue966xo7fTsQWLGx2+8PBWyXAjR9mziPFgoLHoiNe5rHkbUBxJ5P5VSpWVcZ4V03vaIHJ/9nNass4tmqXLEuLRQ+ZZOe6n6SDutJrrOn+xVCA2opVYhv2GQyjnhNbWy/CsCO9m7NXpf7jasq2uZUC+OcEWHQ/b7D0ikDESPjjGENYC8hQ1VdYBwofp7UQCDrKDy1xvaQa6lHTqtjAhA/UW906QabpftkOxG6/wDplUJONxY9aSGr3BOgOvCetRemApWB/hcTbyhihlz+D0XgzL6juay59mH2aX+8Q/7/T4lJNXQc=" 23 | 24 | cache: 25 | directories: 26 | - $HOME/.ivy2/cache 27 | - $HOME/.sbt 28 | 29 | before_cache: 30 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete 31 | - find $HOME/.sbt -name "*.lock" -print -delete -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Evolution Gaming 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kryo Macros [![Build Status](https://travis-ci.org/evolution-gaming/kryo-macros.svg)](https://travis-ci.org/evolution-gaming/kryo-macros) [![license](http://img.shields.io/:license-Apache%202-green.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) [ ![version](https://api.bintray.com/packages/evolutiongaming/maven/kryo-macros/images/download.svg) ](https://bintray.com/evolutiongaming/maven/kryo-macros/_latestVersion) 2 | 3 | Scala macros that generate `com.esotericsoftware.kryo.Serializer` implementations in compile time, based on compile time reflection. 4 | 5 | ## Features and limitations 6 | 7 | - On top level only case classes are supported 8 | - Fields of case classes can be other case classes, Scala collections, options, primitive or `AnyVal` types & classes, 9 | tuples, Scala enums, standard types & classes: `String`, `Either`, `BigDecimal`, `java.time.Instant`, 10 | `scala.concurrent.duration.FiniteDuration`, `org.joda.time.DateTime` 11 | - Fields can be annotated as transient or just be not defined in constructor to avoid parsing and serializing 12 | - For nested structures need to generate serializers for all case classes 13 | - Implicitly defined mapping helpers are supported for ADT structures, simple alternative mappings, etc. 14 | - Manual serializers can be used in generated code when defined as implicits 15 | 16 | ## How to use 17 | 18 | Add the following resolver 19 | ```sbt 20 | resolvers += Resolver.bintrayRepo("evolutiongaming", "maven") 21 | ``` 22 | 23 | Add the library to your dependencies list 24 | ```sbt 25 | libraryDependencies += "com.evolutiongaming" %% "kryo-macros" % "1.3.0" 26 | ``` 27 | 28 | Generate some serializers for your case classes 29 | ```scala 30 | import com.evolutiongaming.kryo.Serializer 31 | 32 | case class Player(name: String) 33 | 34 | val serializer = Serializer.make[Player] 35 | ``` 36 | 37 | That's it! You have generated a `com.esotericsoftware.kryo.Serializer` implementation for your `Player`. 38 | You must know what to do with it if you are here :) 39 | 40 | To serialize objects that extends sealed traits/class use `Serializer.makeCommon` call: 41 | ```scala 42 | import com.evolutiongaming.kryo.{ConstSerializer, Serializer} 43 | 44 | sealed trait Reason 45 | 46 | object Reason { 47 | case object Close extends Reason 48 | case object Pause extends Reason 49 | } 50 | 51 | val reasonSerializer = Serializer.makeCommon[Reason] { 52 | case 0 => ConstSerializer(Reason.Close) 53 | case 1 => ConstSerializer(Reason.Pause) 54 | } 55 | 56 | sealed abstract class Message(val text: String) 57 | 58 | object Message { 59 | case object Common extends Message("common") 60 | case object Notification extends Message("notification") 61 | } 62 | 63 | private implicit val messageSerializer = Serializer.makeMapping[Message] { 64 | case 0 => Message.Common 65 | case 1 => Message.Notification 66 | } 67 | ``` 68 | 69 | To see generated code just add the following line to your sbt build file 70 | ```sbt 71 | scalacOptions += "-Xmacro-settings:print-serializers" 72 | ``` 73 | 74 | For more examples, please, check out 75 | [SerializerMacroSpec](https://github.com/evolution-gaming/kryo-macros/tree/master/macros/src/test/scala/com/evolutiongaming/kryo/SerializerMacroSpec.scala) 76 | 77 | ## How to develop 78 | 79 | ### Run tests, check coverage & binary compatibility for both supported Scala versions 80 | ```sh 81 | sbt clean +coverage +test +coverageReport +mimaReportBinaryIssues 82 | ``` 83 | 84 | ### Run benchmarks 85 | ```sh 86 | sbt -no-colors clean 'benchmark/jmh:run -prof gc .*SerializerBenchmark.*' >results.txt 87 | ``` 88 | 89 | ### Release 90 | 91 | For version numbering use [Recommended Versioning Scheme](http://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html#recommended-versioning-scheme) 92 | that is widely adopted in the Scala ecosystem. 93 | 94 | Double-check binary & source compatibility and release using following command (credentials required): 95 | 96 | ```sh 97 | sbt release 98 | ``` 99 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/com/evolutiongaming/kryo/SerializerBenchmark.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import com.esotericsoftware.kryo.io.{Input, Output} 6 | import org.openjdk.jmh.annotations._ 7 | 8 | import scala.collection.immutable.{BitSet, IntMap, LongMap} 9 | import scala.collection.mutable 10 | 11 | @State(Scope.Thread) 12 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 13 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 14 | @Fork(value = 3, jvmArgs = Array( 15 | "-server", 16 | "-Xms1g", 17 | "-Xmx1g", 18 | "-XX:NewSize=512m", 19 | "-XX:MaxNewSize=512m", 20 | "-XX:InitialCodeCacheSize=256m", 21 | "-XX:ReservedCodeCacheSize=256m", 22 | "-XX:+UseParallelGC", 23 | "-XX:-UseBiasedLocking", 24 | "-XX:+AlwaysPreTouch" 25 | )) 26 | class SerializerBenchmark { 27 | private val kryo = new com.esotericsoftware.kryo.Kryo 28 | private val buf = new Array[Byte](1024) 29 | private val in = new Input(buf) 30 | private val out = new Output(buf) 31 | private val anyRefsSerializer = Serializer.make[AnyRefs] 32 | private val iterablesSerializer = Serializer.make[Iterables] 33 | private val mapsSerializer = Serializer.make[Maps] 34 | private val mutableMapsSerializer = Serializer.make[MutableMaps] 35 | private val intAndLongMapsSerializer = Serializer.make[IntAndLongMaps] 36 | private val bitSetsSerializer = Serializer.make[BitSets] 37 | private val primitivesSerializer = Serializer.make[Primitives] 38 | var anyRefsObj = AnyRefs("s", 1, Some("os")) 39 | var iterablesObj = Iterables(List("1", "2", "3"), Set(4, 5, 6), List(Set(1, 2), Set())) 40 | var mapsObj = Maps(Map("1" -> 1.1, "2" -> 2.2), Map(1 -> Map(3L -> 3.3), 2 -> Map.empty[Long, Double])) 41 | var mutableMapsObj = MutableMaps(mutable.Map("1" -> 1.1, "2" -> 2.2), 42 | mutable.LinkedHashMap(1 -> mutable.OpenHashMap(3L -> 3.3), 2 -> mutable.OpenHashMap.empty[Long, Double])) 43 | var intAndLongMapsObj = IntAndLongMaps(IntMap(1 -> 1.1, 2 -> 2.2), 44 | LongMap(1L -> mutable.LongMap(3L -> 3.3), 2L -> mutable.LongMap.empty[Double])) 45 | var bitSetsObj = BitSets(BitSet(1, 2, 3), mutable.BitSet(1001, 1002, 1003)) 46 | var primitivesObj = Primitives(1, 2, 3, 4, bl = true, 'V', 1.1, 2.2f) 47 | 48 | @Benchmark 49 | def writeThanReadAnyRefs(): AnyRefs = writeThanRead(anyRefsSerializer, anyRefsObj) 50 | 51 | @Benchmark 52 | def writeThanReadIterables(): Iterables = writeThanRead(iterablesSerializer, iterablesObj) 53 | 54 | @Benchmark 55 | def writeThanReadMaps(): Maps = writeThanRead(mapsSerializer, mapsObj) 56 | 57 | @Benchmark 58 | def writeThanReadMutableMaps(): MutableMaps = writeThanRead(mutableMapsSerializer, mutableMapsObj) 59 | 60 | @Benchmark 61 | def writeThanReadIntAndLongMaps(): IntAndLongMaps = writeThanRead(intAndLongMapsSerializer, intAndLongMapsObj) 62 | 63 | @Benchmark 64 | def writeThanReadBitSets(): BitSets = writeThanRead(bitSetsSerializer, bitSetsObj) 65 | 66 | @Benchmark 67 | def writeThanReadPrimitives(): Primitives = writeThanRead(primitivesSerializer, primitivesObj) 68 | 69 | private def writeThanRead[T](s: com.esotericsoftware.kryo.Serializer[T], obj: T): T = { 70 | out.setBuffer(buf) 71 | kryo.writeObject(out, obj, s) 72 | in.setBuffer(buf) 73 | kryo.readObject(in, obj.getClass, s) 74 | } 75 | } 76 | 77 | case class AnyRefs(s: String, bd: BigDecimal, os: Option[String]) 78 | 79 | case class Iterables(l: List[String], s: Set[Int], ls: List[Set[Int]]) 80 | 81 | case class Maps(m: Map[String, Double], mm: Map[Int, Map[Long, Double]]) 82 | 83 | case class MutableMaps(m: mutable.Map[String, Double], mm: mutable.LinkedHashMap[Int, mutable.OpenHashMap[Long, Double]]) 84 | 85 | case class IntAndLongMaps(m: IntMap[Double], mm: LongMap[mutable.LongMap[Double]]) 86 | 87 | case class BitSets(b1: BitSet, b2: mutable.BitSet) 88 | 89 | case class Primitives(b: Byte, s: Short, i: Int, l: Long, bl: Boolean, ch: Char, dbl: Double, f: Float) 90 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/com/evolutiongaming/kryo/SerializerBenchmarkSpec.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | class SerializerBenchmarkSpec extends AnyWordSpec with Matchers { 7 | val benchmark = new SerializerBenchmark 8 | 9 | "SerializerBenchmark" should { 10 | s"write and than read to the same value" in { 11 | benchmark.writeThanReadAnyRefs() shouldEqual benchmark.anyRefsObj 12 | benchmark.writeThanReadIterables() shouldEqual benchmark.iterablesObj 13 | benchmark.writeThanReadMaps() shouldEqual benchmark.mapsObj 14 | benchmark.writeThanReadMutableMaps() shouldEqual benchmark.mutableMapsObj 15 | benchmark.writeThanReadIntAndLongMaps() shouldEqual benchmark.intAndLongMapsObj 16 | benchmark.writeThanReadBitSets() shouldEqual benchmark.bitSetsObj 17 | benchmark.writeThanReadPrimitives() shouldEqual benchmark.primitivesObj 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys.{libraryDependencies, scalacOptions} 2 | import com.typesafe.tools.mima.plugin.MimaPlugin.mimaDefaultSettings 3 | 4 | import scala.sys.process._ 5 | 6 | lazy val oldVersion = "git describe --abbrev=0".!!.trim.replaceAll("^v", "") 7 | 8 | def mimaSettings = mimaDefaultSettings ++ Seq( 9 | mimaCheckDirection := { 10 | def isPatch = { 11 | val Array(newMajor, newMinor, _) = version.value.split('.') 12 | val Array(oldMajor, oldMinor, _) = oldVersion.split('.') 13 | newMajor == oldMajor && newMinor == oldMinor 14 | } 15 | 16 | if (isPatch) "both" else "backward" 17 | }, 18 | mimaPreviousArtifacts := { 19 | def isCheckingRequired = { 20 | val Array(newMajor, newMinor, _) = version.value.split('.') 21 | val Array(oldMajor, oldMinor, _) = oldVersion.split('.') 22 | newMajor == oldMajor && (newMajor != "0" || newMinor == oldMinor) 23 | } 24 | 25 | if (isCheckingRequired) Set(organization.value %% moduleName.value % oldVersion) 26 | else Set() 27 | } 28 | ) 29 | 30 | lazy val commonSettings = Seq( 31 | organization := "com.evolutiongaming", 32 | scalaVersion := "2.13.0", 33 | crossScalaVersions := Seq("2.13.0", "2.12.8"), 34 | releaseCrossBuild := true, 35 | startYear := Some(2016), 36 | organizationName := "Evolution Gaming", 37 | organizationHomepage := Some(url("https://www.evolutiongaming.com/")), 38 | bintrayOrganization := Some("evolutiongaming"), 39 | resolvers += Resolver.bintrayRepo("evolutiongaming", "maven"), 40 | homepage := Some(url("https://github.com/evolution-gaming/kryo-macros")), 41 | licenses := Seq(("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))), 42 | scalacOptions ++= Seq( 43 | "-encoding", "UTF-8", 44 | "-target:jvm-1.8", 45 | "-feature", 46 | "-unchecked", 47 | "-deprecation", 48 | "-Xlint", 49 | "-Ywarn-dead-code", 50 | "-Xmacro-settings:print-serializers" 51 | ) ++ (CrossVersion.partialVersion(scalaVersion.value) match { 52 | case Some((2, x)) if x >= 12 => Seq("-opt:l:method") 53 | case _ => Seq() 54 | }) 55 | ) 56 | 57 | lazy val kryo = project.in(file(".")) 58 | .settings(commonSettings: _*) 59 | .settings( 60 | publish := ((): Unit), 61 | ).aggregate(macros, benchmark) 62 | 63 | lazy val macros = project 64 | .settings(commonSettings: _*) 65 | .settings(mimaSettings: _*) 66 | .settings( 67 | name := "kryo-macros", 68 | libraryDependencies ++= Seq( 69 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 70 | "com.esotericsoftware" % "kryo" % "4.0.2", 71 | "joda-time" % "joda-time" % "2.10.4", 72 | "org.joda" % "joda-convert" % "2.2.1", 73 | "org.scalatest" %% "scalatest" % "3.2.3" % Test 74 | ) 75 | ) 76 | 77 | lazy val benchmark = project 78 | .dependsOn(macros) 79 | .enablePlugins(JmhPlugin) 80 | .settings(commonSettings: _*) 81 | .settings( 82 | name := "kryo-benchmark", 83 | publish := ((): Unit), 84 | libraryDependencies ++= Seq( 85 | "pl.project13.scala" % "sbt-jmh-extras" % "0.3.7", 86 | "org.scalatest" %% "scalatest" % "3.2.3" % Test 87 | ) 88 | ) 89 | -------------------------------------------------------------------------------- /macros/src/main/scala/com/evolutiongaming/kryo/ConstSerializer.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import com.esotericsoftware.kryo 4 | import com.esotericsoftware.kryo.Kryo 5 | import com.esotericsoftware.kryo.io.{Input, Output} 6 | 7 | /** 8 | * Constant Serializer. Always returns `v` value. Specially treated by [[Serializer]] macros. 9 | */ 10 | case class ConstSerializer[T](v: T) extends kryo.Serializer[T] { 11 | override def write(kryo: Kryo, output: Output, `object`: T): Unit = {} 12 | 13 | override def read(kryo: Kryo, input: Input, `type`: Class[T]): T = v 14 | } 15 | -------------------------------------------------------------------------------- /macros/src/main/scala/com/evolutiongaming/kryo/Empty.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | @scala.annotation.meta.field 6 | class Empty extends StaticAnnotation 7 | -------------------------------------------------------------------------------- /macros/src/main/scala/com/evolutiongaming/kryo/Serializer.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | import com.esotericsoftware.kryo 7 | import org.joda.time.DateTime 8 | 9 | import scala.annotation.compileTimeOnly 10 | import scala.collection.immutable.{BitSet, IntMap, LongMap} 11 | import scala.collection.mutable 12 | import scala.concurrent.duration.FiniteDuration 13 | import scala.language.experimental.macros 14 | import scala.reflect.macros.blackbox 15 | 16 | /** 17 | * Macros that generate [[kryo.Serializer]] in compile time, based on compile time reflection. 18 | * 19 | * For more info and examples @see SerializerMacroSpec. 20 | * 21 | * @author Alexander Nemish 22 | */ 23 | object Serializer { 24 | @compileTimeOnly("This method should not be used outside of make/makeMapping/makeCommon macro calls") 25 | def inner[A]: kryo.Serializer[A] = ??? 26 | @compileTimeOnly("This method should not be used outside of make/makeMapping/makeCommon macro calls") 27 | def innerMapping[A](pf: PartialFunction[Int, A]): kryo.Serializer[A] = ??? 28 | @compileTimeOnly("This method should not be used outside of make/makeMapping/makeCommon macro calls") 29 | def innerCommon[A](pf: PartialFunction[Int, kryo.Serializer[_ <: A]]): kryo.Serializer[A] = ??? 30 | def make[A]: kryo.Serializer[A] = macro Macros.serializerImpl[A] 31 | def makeCommon[A](pf: PartialFunction[Int, kryo.Serializer[_ <: A]]): kryo.Serializer[A] = macro Macros.mappingSerializerImpl[A] 32 | def makeMapping[A](pf: PartialFunction[Int, A]): kryo.Serializer[A] = macro Macros.mappingSerializerImpl[A] 33 | 34 | private object Macros { 35 | def serializerImpl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[kryo.Serializer[A]] = { 36 | import c.universe._ 37 | 38 | val tpe = weakTypeOf[A].dealias 39 | 40 | def companion(tpe: Type) = Ident(tpe.typeSymbol.companion) 41 | 42 | def typeArg1(tpe: Type): Type = tpe.typeArgs.head.dealias 43 | 44 | def typeArg2(tpe: Type): Type = tpe.typeArgs.tail.head.dealias 45 | 46 | def hasSingleArgPublicConstructor(tpe: Type) = tpe.decls.exists { 47 | case m: MethodSymbol => 48 | m.isConstructor && m.paramLists.size == 1 && m.paramLists.head.size == 1 && m.isPublic 49 | case _ => false 50 | } 51 | 52 | def isSupportedValueClass(tpe: Type) = 53 | tpe <:< typeOf[AnyVal] && tpe.typeSymbol.isClass && 54 | tpe.typeSymbol.asClass.isDerivedValueClass && hasSingleArgPublicConstructor(tpe) 55 | 56 | def resolveConcreteType(tpe: Type, mtpe: Type): Type = { 57 | val tpeTypeParams = 58 | if (tpe.typeSymbol.isClass) tpe.typeSymbol.asClass.typeParams 59 | else Nil 60 | if (tpeTypeParams.isEmpty) mtpe 61 | else mtpe.substituteTypes(tpeTypeParams, tpe.typeArgs) 62 | } 63 | 64 | def valueClassArg(tpe: Type) = tpe.decls.head // for value classes, first declaration is its single field value 65 | 66 | def valueClassArgType(tpe: Type) = 67 | resolveConcreteType(tpe, valueClassArg(tpe).asMethod.returnType.dealias) 68 | 69 | case class Reader(name: TermName, tree: Tree) 70 | val readers = new mutable.LinkedHashMap[Type, Reader] 71 | 72 | def withReaderFor(tpe: Type)(f: => Tree): Tree = { 73 | val readerName = readers.getOrElseUpdate(tpe, { 74 | val impl = f 75 | val name = TermName(s"r${readers.size}") 76 | val tree = q"""private def $name(kryo: com.esotericsoftware.kryo.Kryo, input: com.esotericsoftware.kryo.io.Input): $tpe = $impl""" 77 | Reader(name, tree)}).name 78 | q"$readerName(kryo, input)" 79 | } 80 | 81 | case class Writer(name: TermName, tree: Tree) 82 | val writers = new mutable.LinkedHashMap[Type, Writer] 83 | 84 | def withWriterFor(tpe: Type, arg: Tree)(f: => Tree): Tree = { 85 | val writerName = writers.getOrElseUpdate(tpe, { 86 | val impl = f 87 | val name = TermName(s"w${writers.size}") 88 | val tree = q"""private def $name(kryo: com.esotericsoftware.kryo.Kryo, output: com.esotericsoftware.kryo.io.Output, x: $tpe): Unit = $impl""" 89 | Writer(name, tree)}).name 90 | q"$writerName(kryo, output, $arg)" 91 | } 92 | 93 | def genCode(m: MethodSymbol, annotations: Set[String]) = { 94 | def findImplicitSerializer(tpe: Type): Option[Tree] = { 95 | val serType = c.typecheck(tq"com.esotericsoftware.kryo.Serializer[$tpe]", mode = c.TYPEmode).tpe 96 | val implSerializer = c.inferImplicitValue(serType) 97 | if (implSerializer != q"") Some(implSerializer) else None 98 | } 99 | 100 | def genWriter(tpe: Type, arg: Tree): Tree = { 101 | lazy val implSerializer = findImplicitSerializer(tpe) 102 | if (tpe.widen =:= definitions.BooleanTpe) { 103 | q"output.writeBoolean($arg)" 104 | } else if (tpe.widen =:= definitions.ByteTpe) { 105 | q"output.writeInt($arg.toInt)" 106 | } else if (tpe.widen =:= definitions.CharTpe) { 107 | q"output.writeInt($arg.toInt)" 108 | } else if (tpe.widen =:= definitions.ShortTpe) { 109 | q"output.writeInt($arg.toInt)" 110 | } else if (tpe.widen weak_<:< definitions.IntTpe) { 111 | q"output.writeInt($arg)" 112 | } else if (tpe.widen =:= definitions.LongTpe) { 113 | q"output.writeLong($arg)" 114 | } else if (tpe.widen =:= definitions.DoubleTpe) { 115 | q"output.writeDouble($arg)" 116 | } else if (tpe.widen =:= definitions.FloatTpe) { 117 | q"output.writeFloat($arg)" 118 | } else if (tpe.widen =:= typeOf[String]) { 119 | q"output.writeString($arg)" 120 | } else if (tpe.widen =:= typeOf[UUID]) { 121 | q"output.writeLong($arg.getMostSignificantBits); output.writeLong($arg.getLeastSignificantBits)" 122 | } else if (tpe =:= typeOf[BigDecimal]) { 123 | q"output.writeString($arg.underlying.toPlainString)" 124 | } else if (tpe =:= typeOf[DateTime]) { 125 | q"output.writeLong($arg.getMillis)" 126 | } else if (tpe =:= typeOf[Instant]) { 127 | q"output.writeLong($arg.toEpochMilli)" 128 | } else if (tpe =:= typeOf[FiniteDuration]) { 129 | q"output.writeLong($arg.toMillis)" 130 | } else if (tpe <:< typeOf[Option[_]]) withWriterFor(tpe, arg) { 131 | val t = typeArg1(tpe) 132 | val emptiable = annotations.contains("com.evolutiongaming.kryo.Empty") 133 | if (emptiable && t =:= typeOf[String]) 134 | q"""output.writeString(if (x.isEmpty) "" else x.get)""" 135 | else if (emptiable && isSupportedValueClass(t) && valueClassArgType(t) =:= typeOf[String]) 136 | q"""output.writeString(if (x.isEmpty) "" else x.get.${valueClassArg(t)})""" 137 | else 138 | q"if (x.isEmpty) output.writeInt(0) else { output.writeInt(1); ${genWriter(t, q"x.get")} }" 139 | } else if (tpe <:< typeOf[Either[_, _]]) withWriterFor(tpe, arg) { 140 | val lf = genWriter(typeArg1(tpe), q"l") 141 | val rf = genWriter(typeArg2(tpe), q"r") 142 | q""" 143 | x match { 144 | case Left(l) => output.writeInt(0); $lf 145 | case Right(r) => output.writeInt(1); $rf 146 | } 147 | """ 148 | } else if (tpe <:< typeOf[mutable.LongMap[_]] || tpe <:< typeOf[LongMap[_]]) withWriterFor(tpe, arg) { 149 | val t = typeArg1(tpe) 150 | val f = genWriter(t, q"kv._2") 151 | q"val s = x.size; output.writeInt(s); if (s > 0) x.foreach { kv => output.writeLong(kv._1); $f }" 152 | } else if (tpe <:< typeOf[IntMap[_]]) withWriterFor(tpe, arg) { 153 | val t = typeArg1(tpe) 154 | val f = genWriter(t, q"kv._2") 155 | q"val s = x.size; output.writeInt(s); if (s > 0) x.foreach { kv => output.writeInt(kv._1); $f }" 156 | } else if (tpe <:< typeOf[mutable.Map[_, _]] || tpe <:< typeOf[Map[_, _]]) withWriterFor(tpe, arg) { 157 | val kf = genWriter(typeArg1(tpe), q"kv._1") 158 | val vf = genWriter(typeArg2(tpe), q"kv._2") 159 | q"val s = x.size; output.writeInt(s); if (s > 0) x.foreach { kv => $kf; $vf }" 160 | } else if (tpe <:< typeOf[BitSet] || tpe <:< typeOf[mutable.BitSet]) withWriterFor(tpe, arg) { 161 | q"val bits = x.toBitMask; output.writeInt(bits.length); output.writeLongs(bits, true)" 162 | } else if (tpe <:< typeOf[Iterable[_]]) withWriterFor(tpe, arg) { 163 | val f = genWriter(typeArg1(tpe), q"a") 164 | q"val s = x.size; output.writeInt(s); if (s > 0) x.foreach(a => $f)" 165 | } else if (isSupportedValueClass(tpe)) { 166 | val value = valueClassArg(tpe) 167 | val t = valueClassArgType(tpe) 168 | genWriter(t, q"$arg.$value") 169 | } else if (tpe.widen <:< typeOf[Enum[_]]) { 170 | q"output.writeString($arg.name)" 171 | } else if (tpe.widen <:< typeOf[Enumeration#Value]) { 172 | q"output.writeString($arg.toString)" 173 | } else if (implSerializer.isDefined) { 174 | q"${implSerializer.get}.write(kryo, output, $arg)" 175 | } else { 176 | q"kryo.writeClassAndObject(output, $arg)" 177 | } 178 | } 179 | 180 | def genReader(tpe: Type): Tree = { 181 | lazy val implSerializer = findImplicitSerializer(tpe) 182 | if (tpe.widen =:= definitions.BooleanTpe) { 183 | q"input.readBoolean" 184 | } else if (tpe.widen =:= definitions.ByteTpe) { 185 | q"input.readInt.toByte" 186 | } else if (tpe.widen =:= definitions.CharTpe) { 187 | q"input.readInt.toChar" 188 | } else if (tpe.widen =:= definitions.ShortTpe) { 189 | q"input.readInt.toShort" 190 | } else if (tpe.widen =:= definitions.IntTpe) { 191 | q"input.readInt" 192 | } else if (tpe.widen weak_<:< definitions.IntTpe) { // handle Byte, Short, Char, Int 193 | q"input.readInt.asInstanceOf[${tpe.widen}]" 194 | } else if (tpe.widen =:= definitions.LongTpe) { 195 | q"input.readLong" 196 | } else if (tpe.widen =:= definitions.DoubleTpe) { 197 | q"input.readDouble" 198 | } else if (tpe.widen =:= definitions.FloatTpe) { 199 | q"input.readFloat" 200 | } else if (tpe.widen =:= typeOf[String]) { 201 | q"input.readString" 202 | } else if (tpe.widen =:= typeOf[UUID]) { 203 | q"new java.util.UUID(input.readLong, input.readLong)" 204 | } else if (tpe =:= typeOf[BigDecimal]) { 205 | q"scala.math.BigDecimal(input.readString)" 206 | } else if (tpe =:= typeOf[DateTime]) { 207 | q"new org.joda.time.DateTime(input.readLong)" 208 | } else if (tpe =:= typeOf[Instant]) { 209 | q"java.time.Instant.ofEpochMilli(input.readLong)" 210 | } else if (tpe =:= typeOf[FiniteDuration]) { 211 | q"new scala.concurrent.duration.FiniteDuration(input.readLong, java.util.concurrent.TimeUnit.MILLISECONDS).toCoarsest.asInstanceOf[scala.concurrent.duration.FiniteDuration]" 212 | } else if (tpe <:< typeOf[Option[_]]) withReaderFor(tpe) { 213 | val t = typeArg1(tpe) 214 | val emptiable = annotations.contains("com.evolutiongaming.kryo.Empty") 215 | if (emptiable && t =:= typeOf[String]) 216 | q""" 217 | val rs = input.readString 218 | if (rs eq null) None 219 | else { 220 | val s = rs.trim 221 | if (s.isEmpty) None else Some(s) 222 | } 223 | """ 224 | else if (emptiable && isSupportedValueClass(t) && valueClassArgType(t) =:= typeOf[String]) 225 | q""" 226 | val rs = input.readString 227 | if (rs eq null) None 228 | else { 229 | val s = rs.trim 230 | if (s.isEmpty) None else Some(new $t(s)) 231 | } 232 | """ 233 | else 234 | q"if (input.readInt == 0) None else Some(${genReader(t)})" 235 | } else if (tpe <:< typeOf[Either[_, _]]) withReaderFor(tpe) { 236 | val lf = genReader(typeArg1(tpe)) 237 | val rf = genReader(typeArg2(tpe)) 238 | q"if (input.readInt == 0) scala.util.Left($lf) else scala.util.Right($rf)" 239 | } else if (tpe <:< typeOf[mutable.LongMap[_]]) withReaderFor(tpe) { 240 | val t = typeArg1(tpe) 241 | val f = genReader(t) 242 | val comp = companion(tpe) 243 | q""" 244 | val size = input.readInt 245 | val map = $comp.empty[$t] 246 | var i = 0 247 | while (i < size) { 248 | map.update(input.readLong, $f) 249 | i += 1 250 | } 251 | map 252 | """ 253 | } else if (tpe <:< typeOf[LongMap[_]]) withReaderFor(tpe) { 254 | val t = typeArg1(tpe) 255 | val f = genReader(t) 256 | val comp = companion(tpe) 257 | q""" 258 | val size = input.readInt 259 | var map = $comp.empty[$t] 260 | var i = 0 261 | while (i < size) { 262 | map = map.updated(input.readLong, $f) 263 | i += 1 264 | } 265 | map 266 | """ 267 | } else if (tpe <:< typeOf[IntMap[_]]) withReaderFor(tpe) { 268 | val t = typeArg1(tpe) 269 | val f = genReader(t) 270 | val comp = companion(tpe) 271 | q""" 272 | val size = input.readInt 273 | var map = $comp.empty[$t] 274 | var i = 0 275 | while (i < size) { 276 | map = map.updated(input.readInt, $f) 277 | i += 1 278 | } 279 | map 280 | """ 281 | } else if (tpe <:< typeOf[mutable.Map[_, _]]) withReaderFor(tpe) { 282 | val kt = typeArg1(tpe) 283 | val vt = typeArg2(tpe) 284 | val kf = genReader(kt) 285 | val vf = genReader(vt) 286 | val comp = companion(tpe) 287 | q""" 288 | val size = input.readInt 289 | val map = $comp.empty[$kt, $vt] 290 | var i = 0 291 | while (i < size) { 292 | map.update($kf, $vf) 293 | i += 1 294 | } 295 | map 296 | """ 297 | } else if (tpe <:< typeOf[Map[_, _]]) withReaderFor(tpe) { 298 | val kt = typeArg1(tpe) 299 | val vt = typeArg2(tpe) 300 | val kf = genReader(kt) 301 | val vf = genReader(vt) 302 | val comp = companion(tpe) 303 | q""" 304 | val size = input.readInt 305 | var map = $comp.empty[$kt, $vt] 306 | var i = 0 307 | while (i < size) { 308 | map = map.updated($kf, $vf) 309 | i += 1 310 | } 311 | map 312 | """ 313 | } else if (tpe <:< typeOf[BitSet] || tpe <:< typeOf[mutable.BitSet]) withReaderFor(tpe) { 314 | val comp = companion(tpe) 315 | q""" 316 | val len = input.readInt 317 | if (len > 0) $comp.fromBitMaskNoCopy(input.readLongs(len, true)) 318 | else $comp.empty 319 | """ 320 | } else if (tpe <:< typeOf[Iterable[_]]) withReaderFor(tpe) { 321 | val t = typeArg1(tpe) 322 | val f = genReader(t) 323 | val comp = companion(tpe) 324 | q""" 325 | val size = input.readInt 326 | if (size > 0) { 327 | val builder = $comp.newBuilder[$t] 328 | var i = 0 329 | do { 330 | builder += $f 331 | i += 1 332 | } while (i < size) 333 | builder.result 334 | } else $comp.empty[$t] 335 | """ 336 | } else if (isSupportedValueClass(tpe)) { 337 | val reader = genReader(valueClassArgType(tpe)) 338 | q"new $tpe($reader)" 339 | } else if (tpe.widen <:< typeOf[Enum[_]]) { 340 | val comp = companion(tpe) 341 | q"$comp.valueOf(input.readString)" 342 | } else if (tpe.widen <:< typeOf[Enumeration#Value]) { 343 | val TypeRef(SingleType(_, enumSymbol), _, _) = tpe 344 | q"$enumSymbol.withName(input.readString)" 345 | } else if (implSerializer.isDefined) { 346 | q"${implSerializer.get}.read(kryo, input, null)" 347 | } else { 348 | q"kryo.readClassAndObject(input).asInstanceOf[$tpe]" 349 | } 350 | } 351 | 352 | val mtpe = resolveConcreteType(tpe, m.returnType.dealias) 353 | val writer = genWriter(mtpe, q"x.$m") 354 | val reader = genReader(mtpe) 355 | val namedArgReader = q"$m = $reader" 356 | writer -> namedArgReader 357 | } 358 | 359 | if (!(tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass)) 360 | c.error(c.enclosingPosition, s"$tpe must be a case class.") 361 | 362 | val annotations = tpe.members.collect { 363 | case m: TermSymbol if { 364 | m.info // to enforce the type information completeness and availability of annotations 365 | m.annotations.nonEmpty 366 | } => m.getter -> m.annotations.map(_.toString).toSet 367 | }.toMap 368 | 369 | def notTransient(m: MethodSymbol) = !annotations.get(m).exists(_.contains("transient")) 370 | 371 | val fields = tpe.members.toSeq.reverse.collect { 372 | case m: MethodSymbol if m.isCaseAccessor && notTransient(m) => 373 | genCode(m, annotations.getOrElse(m, Set.empty)) 374 | } 375 | 376 | val (write, read) = fields.unzip 377 | val createObject = q"new $tpe(..$read)" 378 | val tree = 379 | q"""new com.esotericsoftware.kryo.Serializer[$tpe] { 380 | setImmutable(true) 381 | def write(kryo: com.esotericsoftware.kryo.Kryo, output: com.esotericsoftware.kryo.io.Output, x: $tpe): Unit = { ..$write } 382 | def read(kryo: com.esotericsoftware.kryo.Kryo, input: com.esotericsoftware.kryo.io.Input, `type`: Class[$tpe]): $tpe = $createObject 383 | ..${readers.values.map(_.tree)} 384 | ..${writers.values.map(_.tree)} 385 | }""" 386 | 387 | if (c.settings.contains("print-serializers")) { 388 | val code = showCode(tree) 389 | c.info(c.enclosingPosition, s"Generated kryo.Serializer for type $tpe:\n$code", force = true) 390 | } 391 | c.Expr[kryo.Serializer[A]](tree) 392 | } 393 | 394 | def mappingSerializerImpl[A: c.WeakTypeTag](c: blackbox.Context)(pf: c.Tree): c.Expr[kryo.Serializer[A]] = { 395 | import c.universe._ 396 | 397 | case class InnerSerializer(name: TermName, tree: Tree) 398 | val serializers = new mutable.LinkedHashMap[Tree, InnerSerializer] 399 | 400 | val tpe = weakTypeOf[A] 401 | val q"{ case ..$cases }" = pf 402 | 403 | val constSerializerType = c.typecheck(tq"com.evolutiongaming.kryo.ConstSerializer.type", mode = c.TYPEmode).tpe 404 | 405 | val updatedCases = cases collect { 406 | case cq"$pat => $expr.inner[$tpe]" if expr.tpe =:= typeOf[Serializer.type] => 407 | val fieldName = c.freshName(tpe.tpe.toString.replace('.', '$')) 408 | val is = serializers.getOrElseUpdate(tpe, InnerSerializer(TermName(fieldName), q"Serializer.make[$tpe]")) 409 | val write = cq"t: $tpe => { output.writeInt($pat); ${is.name}.write(kryo, output, t) }" 410 | val read = cq"$pat => ${is.name}.read(kryo, input, null)" 411 | write -> read 412 | case cq"$pat => $expr.innerMapping[$tpe]($pf)" if expr.tpe =:= typeOf[Serializer.type] => 413 | val fieldName = c.freshName(tpe.tpe.toString.replace('.', '$')) 414 | val is = serializers.getOrElseUpdate(tpe, InnerSerializer(TermName(fieldName), q"Serializer.makeMapping[$tpe]($pf)")) 415 | val write = cq"t: $tpe => { output.writeInt($pat); ${is.name}.write(kryo, output, t) }" 416 | val read = cq"$pat => ${is.name}.read(kryo, input, null)" 417 | write -> read 418 | case cq"$pat => $expr.innerCommon[$tpe]($pf)" if expr.tpe =:= typeOf[Serializer.type] => 419 | val fieldName = c.freshName(tpe.tpe.toString.replace('.', '$')) 420 | val is = serializers.getOrElseUpdate(tpe, InnerSerializer(TermName(fieldName), q"Serializer.makeCommon[$tpe]($pf)")) 421 | val write = cq"t: $tpe => { output.writeInt($pat); ${is.name}.write(kryo, output, t) }" 422 | val read = cq"$pat => ${is.name}.read(kryo, input, null)" 423 | write -> read 424 | case cq"$pat => $constSerializer.apply[$tpe]($v)" if constSerializer.tpe =:= constSerializerType => 425 | val write = cq"t: $tpe => output.writeInt($pat)" 426 | val read = cq"$pat => $v" 427 | write -> read 428 | case cq"$pat => $expr" if expr.tpe.baseClasses.contains(weakTypeOf[kryo.Serializer[A]].typeSymbol) => 429 | val serializerType = expr.tpe.baseType(weakTypeOf[kryo.Serializer[A]].typeSymbol).typeArgs.head.dealias 430 | val write = cq"t: $serializerType => { output.writeInt($pat); $expr.write(kryo, output, t) }" 431 | val read = cq"$pat => $expr.read(kryo, input, null)" 432 | write -> read 433 | case read@cq"$pat => $expr" => 434 | val write = cq"$expr => output.writeInt($pat)" 435 | write -> read 436 | } 437 | 438 | val (updatedWriteCases, updatedReadCases) = updatedCases.unzip 439 | val write = q"x match { case ..$updatedWriteCases}" 440 | val read = q"(input.readInt: @scala.annotation.switch) match { case ..$updatedReadCases }" 441 | 442 | val innerSerializers = serializers.map { 443 | case (tpe, InnerSerializer(name, tree)) => q"private val $name: com.esotericsoftware.kryo.Serializer[$tpe] = $tree" 444 | } 445 | val tree = 446 | q"""new com.esotericsoftware.kryo.Serializer[$tpe] { 447 | ..$innerSerializers 448 | def write(kryo: com.esotericsoftware.kryo.Kryo, output: com.esotericsoftware.kryo.io.Output, x: $tpe): Unit = { ..$write } 449 | def read(kryo: com.esotericsoftware.kryo.Kryo, input: com.esotericsoftware.kryo.io.Input, `type`: Class[$tpe]): $tpe = $read 450 | }""" 451 | if (c.settings.contains("print-serializers")) { 452 | val code = showCode(tree) 453 | c.info(c.enclosingPosition, s"Generated kryo.Serializer for type $tpe:\n$code", force = true) 454 | } 455 | c.Expr[kryo.Serializer[A]](tree) 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /macros/src/test/java/com/evolutiongaming/kryo/Suit.java: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo; 2 | 3 | public enum Suit { 4 | Hearts, Spades, Diamonds, Clubs; 5 | } -------------------------------------------------------------------------------- /macros/src/test/scala/com/evolutiongaming/kryo/SerializerMacroSpec.scala: -------------------------------------------------------------------------------- 1 | package com.evolutiongaming.kryo 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.time.Instant 5 | import java.util.UUID 6 | import java.util.concurrent.TimeUnit 7 | 8 | import com.esotericsoftware.kryo.Kryo 9 | import com.esotericsoftware.kryo.io.{Input, Output} 10 | import com.esotericsoftware.{kryo => k} 11 | import org.joda.time.DateTime 12 | import org.scalatest.exceptions.TestFailedException 13 | 14 | import scala.collection.immutable.{BitSet, IntMap, List, LongMap} 15 | import scala.collection.mutable 16 | import scala.concurrent.duration._ 17 | import org.scalatest.matchers.should.Matchers 18 | import org.scalatest.wordspec.AnyWordSpec 19 | 20 | case class Id[A](value: A) extends AnyVal 21 | case class PlayerId(value: String) extends AnyVal 22 | case class IntId(value: Int) extends AnyVal 23 | class ValWithPrivateConstructor private(val value: String) extends AnyVal 24 | object ValWithPrivateConstructor { 25 | def apply(value: String): ValWithPrivateConstructor = new ValWithPrivateConstructor(value) 26 | } 27 | 28 | /** 29 | * These are test and examples of using [[Serializer]] macros for 30 | * generating [[k.Serializer]] for our case classes. 31 | * 32 | * scalacOptions := Seq(..., "-Xmacro-settings:print-serializers", ...) 33 | * 34 | * First, these macros only work for case classes. Otherwise, you need to write your serializers manually. 35 | * 36 | * Below tests demonstrate what is currently supported by the serializers. 37 | * 38 | * One can use type aliases. 39 | * 40 | * When you need a field not to be serialized, annotate it with [[transient]] annotation. 41 | * 42 | * When you need an Option[String] field to be serialized as an empty String in case of None, 43 | * annotate it with [[Empty]] annotation. 44 | * This is used for legacy serializers compatibility purposes. 45 | * 46 | * @usecase 47 | * case class YourClass(@Empty legacyField: Option[String]) 48 | * val s = Serializer.make[YourClass] 49 | * 50 | * Will generate: 51 | * {{{ 52 | * final class $anon extends kryo.Serializer[YourClass] { 53 | * setImmutable(true); 54 | * def write(kryo: Kryo, output: Output, x: YourClass): Unit = output.writeString(x.legacyField.getOrElse("")); 55 | * def read(kryo: Kryo, input: Input, `type`: java.lang.Class[YourClass]): YourClass = new YourClass( 56 | * legacyField = Option(input.readString).map(_.trim).filter(_.nonEmpty)) 57 | * } 58 | * }}} 59 | * 60 | * @author Alexander Nemish 61 | */ 62 | class SerializerMacroSpec extends AnyWordSpec with Matchers { 63 | object SuitEnum extends Enumeration { 64 | type SuitEnum = Value 65 | val Hearts: SuitEnum = Value(1, "Hearts") // WARNING: for most efficiency set names explicitly in your enumerations, 66 | val Spades: SuitEnum = Value(2, "Spades") // you still not sure then please look and check that following 67 | val Diamonds: SuitEnum = Value(3, "Diamonds") // synchronized block will not affect your system and its performance: 68 | val Clubs: SuitEnum = Value(4, "Clubs") // https://github.com/scala/scala/blob/1692ae306dc9a5ff3feebba6041348dfdee7cfb5/src/library/scala/Enumeration.scala#L203 69 | } 70 | 71 | lazy val kryo = new Kryo 72 | 73 | "Serializer" should { 74 | "serialize and deserialize primitives" in { 75 | case class Primitives(b: Byte, s: Short, i: Int, l: Long, bl: Boolean, ch: Char, double: Double, f: Float) 76 | 77 | verify(Serializer.make[Primitives], Primitives(1, 2, 3, 4, true, 'V', 1.1, 2.2f)) 78 | } 79 | 80 | "serialize and deserialize standard types" in { 81 | case class Types(str: String, bd: BigDecimal, id: UUID, dt: DateTime, inst: Instant, dur: FiniteDuration) 82 | 83 | verify(Serializer.make[Types], Types("test", 3, new UUID(1L, 2L), DateTime.now(), Instant.now(), 84 | FiniteDuration(1234, TimeUnit.MILLISECONDS))) 85 | } 86 | 87 | /** 88 | * Value classes derived from [[AnyVal]] 89 | */ 90 | "serialize and deserialize value classes" in { 91 | case class ValueClassTypes(id: PlayerId, intId: IntId) 92 | 93 | verify(Serializer.make[ValueClassTypes], ValueClassTypes(PlayerId("id"), IntId(123))) 94 | } 95 | 96 | "serialize and deserialize Options" in { 97 | case class Opt(a: Option[String], b: Option[Int]) 98 | 99 | verify(Serializer.make[Opt], Opt(None, Some(1))) 100 | } 101 | 102 | "serialize and deserialize tuples" in { 103 | verify(Serializer.make[(Int, String, Option[IntId])], (1, "VVV", Some(IntId(2)))) 104 | } 105 | 106 | "serialize and deserialize Either" in { 107 | case class Eith(left: Either[String, Int], right: Either[String, Int]) 108 | 109 | verify(Serializer.make[Eith], Eith(Left("Error"), Right(42))) 110 | } 111 | 112 | "serialize and deserialize Enumeration & java.lang.Enum types" in { 113 | case class Enums(enum: SuitEnum.SuitEnum, javaEnum: Suit) 114 | verify(Serializer.make[Enums], Enums(SuitEnum.Spades, Suit.Hearts)) 115 | } 116 | 117 | "serialize and deserialize Iterable types" in { 118 | import SuitEnum._ 119 | 120 | case class Iter(ss: Set[SuitEnum], is: List[Int], ls: mutable.ArrayBuffer[Long]) 121 | 122 | verify(Serializer.make[Iter], Iter(Set(Hearts, Spades, Diamonds), List(1, 2, 3), mutable.ArrayBuffer(5L, 6L))) 123 | } 124 | 125 | "serialize and deserialize Map types" in { 126 | case class Maps(m1: Map[Int, String], m2: mutable.HashMap[Long, Double]) 127 | 128 | verify(Serializer.make[Maps], Maps(Map(1 -> "one"), mutable.HashMap(2L -> 2.2))) 129 | } 130 | 131 | "serialize and deserialize IntMap & LongMap types" in { 132 | case class IntAndLongMaps(m1: IntMap[String], m2: LongMap[Double], m3: mutable.LongMap[Int]) 133 | 134 | verify(Serializer.make[IntAndLongMaps], IntAndLongMaps(IntMap(1 -> "one"), LongMap(2L -> 2.2), mutable.LongMap(3L -> 3))) 135 | } 136 | 137 | "serialize and deserialize BitSet types" in { 138 | case class BitSets(b1: BitSet, b2: mutable.BitSet) 139 | 140 | verify(Serializer.make[BitSets], BitSets(BitSet(1, 2, 3), mutable.BitSet(1001, 1002, 1003))) 141 | } 142 | 143 | "serialize and deserialize empty strings when option string field is annotated by Empty" in { 144 | case class OptionString(@Empty s: Option[String]) 145 | 146 | case class EmptyString(s: String) 147 | 148 | val optionStringSerializer = Serializer.make[OptionString] 149 | val emptyStringSerializer = Serializer.make[EmptyString] 150 | verifyFromTo(optionStringSerializer, OptionString(Option("VVV")), emptyStringSerializer, EmptyString("VVV")) 151 | verifyFromTo(optionStringSerializer, OptionString(None), emptyStringSerializer, EmptyString("")) 152 | verifyFromTo(emptyStringSerializer, EmptyString("VVV"), optionStringSerializer, OptionString(Option("VVV"))) 153 | verifyFromTo(emptyStringSerializer, EmptyString(""), optionStringSerializer, OptionString(None)) 154 | verifyFromTo(emptyStringSerializer, EmptyString(" "), optionStringSerializer, OptionString(None)) 155 | } 156 | 157 | "don't serialize and deserialize field is annotated by transient or just is not defined in constructor" in { 158 | case class Transient(r: String, @transient t: String = "a") { 159 | val ignored: String = "i" + r 160 | } 161 | 162 | val transientSerializer = Serializer.make[Transient] 163 | 164 | case class Required(s: String) 165 | 166 | val requiredSerializer = Serializer.make[Required] 167 | verifyFromTo(transientSerializer, Transient("VVV"), requiredSerializer, Required("VVV")) 168 | verifyFromTo(requiredSerializer, Required("VVV"), transientSerializer, Transient("VVV")) 169 | } 170 | 171 | "serialize and deserialize case classes and value classes with first-order type parameters" in { 172 | case class FirstOrderType[A, B](a: A, b: B, oa: Option[A], bs: List[B]) 173 | 174 | verify(Serializer.make[FirstOrderType[Int, String]], 175 | FirstOrderType[Int, String](1, "VVV", Some(2), List("VVV", "WWW"))) 176 | verify(Serializer.make[FirstOrderType[Id[Int], Id[String]]], 177 | FirstOrderType[Id[Int], Id[String]](Id(1), Id("VVV"), Some(Id(2)), List(Id("VVV"), Id("WWW")))) 178 | } 179 | 180 | "don't generate serializer for first-order types that are specified using 'Any' type parameter" in { 181 | assert(intercept[TestFailedException](assertCompiles { 182 | """case class FirstOrderType[A](a: A) 183 | |Serializer.make[FirstOrderType[_]]""".stripMargin 184 | }).getMessage.contains ("class type required but FirstOrderType[_] found")) 185 | } 186 | /* 187 | "serialize and deserialize case classes with higher-kinded type parameters" in { 188 | import scala.language.higherKinds 189 | 190 | case class HigherKindedType[F[_]](f: F[Int], fs: F[HigherKindedType[F]]) 191 | 192 | verify(Serializer.make[HigherKindedType[Option]], 193 | HigherKindedType[Option](Some(1), Some(HigherKindedType[Option](Some(2), None)))) 194 | verify(Serializer.make[HigherKindedType[List]], 195 | HigherKindedType[List](List(1), List(HigherKindedType[List](List(2), Nil)))) 196 | } 197 | */ 198 | 199 | /** 200 | * When [[Serializer]] finds an implicit [[k.Serializer]] for given field's type 201 | * it uses it instead of generating `out.writeAny`/`in.readAny` 202 | * 203 | * Generated serializer would be: 204 | * class $anon extends kryo.Serializer[Outer] { 205 | * def write(kryo: Kryo, output: Output, `object`: Outer): Unit = { 206 | * implicitSerializerInnerA.write(kryo, output, x.a); 207 | * ManualSerializerInnerB.write(kryo, output, x.b) 208 | * }; 209 | * def read(kryo: Kryo, input: Input, `type`: Class[Outer]): Outer = new Outer( 210 | * a = implicitSerializerInnerA.read(kryo, input, null), 211 | * b = ManualSerializerInnerB.read(kryo, input, null)) 212 | * }; 213 | */ 214 | "serialize and deserialize using implicit serializers" in { 215 | case class InnerA(i: Int) 216 | case class InnerB(s: String) 217 | case class Outer(a: InnerA, b: InnerB) 218 | 219 | implicit val implicitSerializerInnerA = Serializer.make[InnerA] 220 | 221 | implicit object ManualSerializerInnerB extends k.Serializer[InnerB] { 222 | override def write(kryo: Kryo, output: Output, `object`: InnerB): Unit = output.writeString(`object`.s) 223 | override def read(kryo: Kryo, input: Input, `type`: Class[InnerB]): InnerB = InnerB(input.readString()) 224 | } 225 | 226 | verify(Serializer.make[Outer], Outer(InnerA(1), InnerB("b"))) 227 | } 228 | 229 | /** 230 | * Sometime it's useful to serialize a mapping, like below. Only simple mapping are supported. 231 | * For something more complex please write a serializer manually. 232 | * 233 | * Serializer generated would be: 234 | * {{{ 235 | * final class $anon extends kryo.Serializer[SuitEnum.SuitEnum] { 236 | * def write(kryo: Kryo, output: Output, x: SuitEnum.SuitEnum): Unit = x match { 237 | * case SuitEnum.Hearts => output.writeInt(1) 238 | * case SuitEnum.Spades => output.writeInt(2) 239 | * case SuitEnum.Diamonds => output.writeInt(3) 240 | * case SuitEnum.Clubs => output.writeInt(4) 241 | * }; 242 | * def read(kryo: Kryo, input: Input, `type`: Class[InnerB]): SuitEnum.SuitEnum = input.readInt match { 243 | * case 1 => SuitEnum.Hearts 244 | * case 2 => SuitEnum.Spades 245 | * case 3 => SuitEnum.Diamonds 246 | * case 4 => SuitEnum.Clubs 247 | * } 248 | * }; 249 | * 250 | * }}} 251 | */ 252 | "serialize and deserialize mappings" in { 253 | val s = Serializer.makeMapping[SuitEnum.SuitEnum] { 254 | case 1 => SuitEnum.Hearts 255 | case 2 => SuitEnum.Spades 256 | case 3 => SuitEnum.Diamonds 257 | case 4 => SuitEnum.Clubs 258 | } 259 | 260 | verify(s, SuitEnum.Spades) 261 | } 262 | 263 | /** 264 | * To serialize a ADT structure, use [[Serializer.makeCommon]] macro. 265 | * It take a [[PartialFunction]] from Int to [[k.Serializer]] that serializes a particular ADT constructor. 266 | * Right-hand side [[k.Serializer]] can be a val, def or object. Either generated or manually written. 267 | */ 268 | "serialize and deserialize Algebraic Data Types using existing serializers" in { 269 | trait User 270 | case class Player(name: String) extends User 271 | case class Dealer(name: String) extends User 272 | 273 | val playerSerializer = Serializer.make[Player] 274 | val dealerSerializer = Serializer.make[Dealer] 275 | 276 | val userSerializer = Serializer.makeCommon[User] { 277 | case 1 => playerSerializer 278 | case 2 => dealerSerializer 279 | } 280 | 281 | verify(userSerializer, Player("satoshi")) 282 | } 283 | 284 | /** 285 | * 286 | * 287 | * When you need to serializer a case object as one of the constructors of algebraic data type, 288 | * (e.g. `God` user), use [[ConstSerializer]] 289 | * It's specially treated by [[Serializer]] macros, no boilerplay is generated. 290 | */ 291 | "serialize and deserialize case objects using existing ConstSerializer" in { 292 | trait User 293 | case class Player(name: String) extends User 294 | case class Dealer(name: String) extends User 295 | case object God extends User 296 | 297 | val playerSerializer = Serializer.make[Player] 298 | val dealerSerializer = Serializer.make[Dealer] 299 | 300 | val userSerializer = Serializer.makeCommon[User] { 301 | case 0 => ConstSerializer(God) 302 | case 1 => playerSerializer 303 | case 2 => dealerSerializer 304 | } 305 | 306 | verify(userSerializer, Player("satoshi")) 307 | } 308 | 309 | /** 310 | * Often it's useful to declare serializers of ADT constructors inside the common serializer. 311 | * It's possible with [[Serializer.inner]] functions. 312 | 313 | * @note You can't use [[Serializer.make]] inside the macros! 314 | * 315 | * There are [[Serializer.inner]], [[Serializer.innerMapping]] and [[Serializer.innerCommon]] 316 | * that should be used inside [[Serializer.makeCommon]] macro, accordingly. 317 | */ 318 | "serialize and deserialize Algebraic Data Types using Serializer.inner macros" in { 319 | sealed trait ColorType 320 | case object Red extends ColorType 321 | case object Black extends ColorType 322 | 323 | sealed trait AlgebraicDataType 324 | case class B(a: String) extends AlgebraicDataType 325 | case class C(a: String) extends AlgebraicDataType 326 | case class D(a: ColorType) extends AlgebraicDataType 327 | 328 | 329 | val outerBSerializer = Serializer.make[B] 330 | 331 | val ss = Serializer.makeCommon[AlgebraicDataType] { 332 | case 1 => outerBSerializer 333 | case 2 => Serializer.inner[C] 334 | case 3 => Serializer.innerMapping[D] { 335 | case 1 => D(Red) 336 | case 2 => D(Black) 337 | } 338 | } 339 | 340 | verify(ss, B("test")) 341 | verify(ss, C("test")) 342 | verify(ss, D(Black)) 343 | } 344 | 345 | "serialize and deserialize respecting type aliases" in { 346 | sealed trait TreeType[A] 347 | case class Node[A](left: Node[A], right: Node[A]) extends TreeType[A] 348 | case class Leaf[A](value: A) extends TreeType[A] 349 | 350 | type AliasToIntTree = TreeType[Int] 351 | type AliasToIntNode = Node[Int] 352 | type AliasToIntLeaf = Leaf[Int] 353 | type AliasToMap = Map[String, String] 354 | type AliasToEither[L] = Either[L, Int] 355 | type AliasToOption = Option[String] 356 | type AliasToSet = Set[Int] 357 | type AliasToString = String 358 | 359 | case class CustomType(a: AliasToString) 360 | case class Aliases(m: AliasToMap, o: AliasToOption, s: AliasToSet, e: AliasToEither[AliasToString], 361 | c: CustomType, t: AliasToIntTree) 362 | 363 | type AliasToAliases = Aliases 364 | type AliasToTreeSerializer = k.Serializer[AliasToIntTree] 365 | type AliasToCustomTypeSerializer = k.Serializer[CustomType] 366 | 367 | implicit val colorSerializer: AliasToTreeSerializer = Serializer.makeCommon[AliasToIntTree] { 368 | case 1 => Serializer.inner[AliasToIntNode] 369 | case 2 => Serializer.inner[AliasToIntLeaf] 370 | } 371 | implicit val customSerializer: AliasToCustomTypeSerializer = Serializer.make[CustomType] 372 | 373 | verify(Serializer.make[AliasToAliases], Aliases(Map("one" -> "1"), Some(""), Set(2), Right(3), CustomType("VVV"), Leaf(4))) 374 | } 375 | 376 | "serialize and deserialize a complex data structure using all possible crap described above" in { 377 | trait GameType 378 | case object Baccarat extends GameType 379 | case object Roulette extends GameType 380 | 381 | case class Game(gameType: GameType) 382 | 383 | object ActionType extends Enumeration { 384 | val StartBetting = Value(1, "StartBetting") 385 | val StopBetting = Value(2, "StopBetting") 386 | } 387 | 388 | trait Command 389 | case class Start(g: Game) extends Command 390 | case object Stop extends Command 391 | case class Action(action: ActionType.Value) extends Command 392 | 393 | implicit val gameTypeSerializer = Serializer.makeMapping[GameType] { 394 | case 1 => Baccarat 395 | case 2 => Roulette 396 | } 397 | 398 | // gameTypeSerializer is used here to serialize `game` field 399 | val gameSerializer = Serializer.make[Game] 400 | 401 | // you can create an implicit def for an existing serializer of needed type 402 | // and Serializer.make macro fill find and use it. 403 | implicit def implGameSerializer = gameSerializer 404 | 405 | val commandSerializer = Serializer.makeCommon[Command] { 406 | case 1 => ConstSerializer(Stop) // use ConstSerializer for case object 407 | case 2 => Serializer.inner[Start] // gameSerializer is used here to serialize `game` field 408 | case 3 => Serializer.innerMapping[Action] { 409 | case 1 => Action(ActionType.StartBetting) 410 | case 2 => Action(ActionType.StopBetting) 411 | } 412 | } 413 | 414 | verify(commandSerializer, Stop) 415 | verify(commandSerializer, Start(Game(Baccarat))) 416 | verify(commandSerializer, Start(Game(Roulette))) 417 | verify(commandSerializer, Action(ActionType.StartBetting)) 418 | verify(commandSerializer, Action(ActionType.StopBetting)) 419 | } 420 | 421 | "serialize and deserialize values classes with private constructors using implicit serializers" in { 422 | case class Outer(a: ValWithPrivateConstructor) 423 | 424 | implicit object ManualSerializer extends k.Serializer[ValWithPrivateConstructor] { 425 | override def write( 426 | kryo: Kryo, output: Output, `object`: ValWithPrivateConstructor 427 | ): Unit = output.writeString(`object`.value) 428 | override def read( 429 | kryo: Kryo, input: Input, `type`: Class[ValWithPrivateConstructor] 430 | ): ValWithPrivateConstructor = ValWithPrivateConstructor(input.readString()) 431 | } 432 | 433 | verify(Serializer.make[Outer], Outer(ValWithPrivateConstructor("a"))) 434 | } 435 | } 436 | 437 | def verify[T <: AnyRef](s: com.esotericsoftware.kryo.Serializer[T], expected: T)(implicit m: Manifest[T]): Unit = { 438 | val bytes = serialize(s, expected) 439 | val actual = deserialize(s, bytes) 440 | actual shouldEqual expected 441 | } 442 | 443 | def verifyFromTo[T <: AnyRef, T2 <: AnyRef](s1: com.esotericsoftware.kryo.Serializer[T], in: T, 444 | s2: com.esotericsoftware.kryo.Serializer[T2], expectedOut: T2) 445 | (implicit m1: Manifest[T], m2: Manifest[T2]): Unit = { 446 | val bytes = serialize(s1, in) 447 | val actual = deserialize(s2, bytes) 448 | actual shouldEqual expectedOut 449 | } 450 | 451 | def deserialize[T <: AnyRef](s: com.esotericsoftware.kryo.Serializer[T], bytes: Array[Byte])(implicit m: Manifest[T]): T = 452 | kryo.readObject(new Input(bytes), m.runtimeClass.asInstanceOf[Class[T]], s) 453 | 454 | def serialize[T <: AnyRef](s: com.esotericsoftware.kryo.Serializer[T], obj: T): Array[Byte] = { 455 | val out = new Output(new ByteArrayOutputStream) 456 | try kryo.writeObject(out, obj, s) 457 | finally out.close() 458 | out.getBuffer 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.5 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.6.1") 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") 4 | 5 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") 6 | 7 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13") 8 | 9 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7") 10 | 11 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.3.0") 12 | -------------------------------------------------------------------------------- /release.sbt: -------------------------------------------------------------------------------- 1 | import scala.sys.process._ 2 | import sbtrelease.ReleaseStateTransformations._ 3 | 4 | lazy val updateVersionInReadme: ReleaseStep = { st: State => 5 | val extracted = Project.extract(st) 6 | val newVersion = extracted.get(version) 7 | val oldVersion = "git describe --abbrev=0".!!.trim.replaceAll("^v", "") 8 | val readme = "README.md" 9 | val oldContent = IO.read(file(readme)) 10 | val newContent = oldContent.replaceAll('"' + oldVersion + '"', '"' + newVersion + '"') 11 | IO.write(file(readme), newContent) 12 | s"git add $readme" !! st.log 13 | st 14 | } 15 | 16 | releaseCrossBuild := true 17 | 18 | releaseProcess := Seq[ReleaseStep]( 19 | checkSnapshotDependencies, 20 | inquireVersions, 21 | runClean, 22 | runTest, 23 | setReleaseVersion, 24 | ReleaseStep(releaseStepCommand("mimaReportBinaryIssues"), enableCrossBuild = true), 25 | updateVersionInReadme, 26 | commitReleaseVersion, 27 | tagRelease, 28 | publishArtifacts, 29 | setNextVersion, 30 | commitNextVersion, 31 | pushChanges 32 | ) -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "1.3.1-SNAPSHOT" 2 | --------------------------------------------------------------------------------