Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: recursive types #1622

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.ing.baker.runtime.serialization

import com.ing.baker.runtime.scaladsl.IngredientInstance
import io.circe.{Codec, Json}
import io.circe.generic.semiauto.deriveCodec
import io.circe.{Codec, Decoder, Encoder, Json}
import io.circe.generic.semiauto.{deriveCodec, deriveDecoder, deriveEncoder}
import com.ing.baker.runtime.serialization.JsonEncoders._
import com.ing.baker.runtime.serialization.JsonDecoders._

Expand All @@ -19,6 +19,8 @@ object JsonCodec {
implicit val listTypeCodec: Codec[ListType] = deriveCodec[ListType]
implicit val listValueCodec: Codec[ListValue] = deriveCodec[ListValue]
implicit val primitiveTypeCodec: Codec[PrimitiveType] = deriveCodec[PrimitiveType]
implicit val referenceTypeEncoder: Encoder[ReferenceType] = Encoder.encodeString.contramap[ReferenceType](_.name)
implicit val referenceTypeDecoder: Decoder[ReferenceType] = Decoder.decodeString.map(name => ReferenceType(TypeLoader.defaultTypeLoader, name))
implicit val typeCodec: Codec[Type] = deriveCodec[Type]

def removeNulls: ((String, Json)) => Boolean = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object Converters extends LazyLogging {


private val configPathPrefix: String = "baker.types"
private val defaultTypeConverter: TypeAdapter = new TypeAdapter(loadDefaultModulesFromConfig())
protected[types] val defaultTypeConverter: TypeAdapter = new TypeAdapter(loadDefaultModulesFromConfig())

@nowarn
def loadDefaultModulesFromConfig(): Map[Class[_], TypeModule] = {
Expand Down
19 changes: 19 additions & 0 deletions core/baker-types/src/main/scala/com/ing/baker/types/Type.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ sealed trait Type {
case (MapType(valueTypeA), MapType(valueTypeB)) => valueTypeA.isAssignableFrom(valueTypeB)

case (RecordType(entriesA), RecordType(entriesB)) =>

// recursion here

val entriesMap: Map[String, Type] = entriesB.map(f => f.name -> f.`type`).toMap
entriesA.forall { f =>
entriesMap.get(f.name) match {
Expand All @@ -45,6 +48,15 @@ sealed trait Type {
}
}

case (ReferenceType(typeLoaderA, stringA), ReferenceType(typeLoaderB, stringB)) =>
typeLoaderA == typeLoaderB && stringA == stringB

case (ReferenceType(typeLoader, name), otherType) =>
typeLoader.loadType(name).map(_.isAssignableFrom(otherType)).getOrElse(false)

case (otherType, ReferenceType(typeLoader, name)) =>
typeLoader.loadType(name).map(otherType.isAssignableFrom).getOrElse(false)

case _ => false
}
}
Expand All @@ -56,6 +68,8 @@ sealed trait Type {
def isMap: Boolean = isInstanceOf[MapType]
def isRecord: Boolean = isInstanceOf[RecordType]

// def getTypeLoader: TypeLoader

override def hashCode(): Int = {
val p1 = 31
val p2 = 11
Expand Down Expand Up @@ -124,6 +138,11 @@ case class MapType(valueType: Type) extends Type {
override def toString: String = s"Map[$valueType]"
}

case class ReferenceType(typeLoader: TypeLoader, name: String) extends Type {

override def toString: String = s"ReferenceType($name)"
}

sealed trait PrimitiveType extends Type

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import com.ing.baker.types.modules._

import scala.reflect.runtime.universe

class TypeAdapter(private val modules: Map[Class[_], TypeModule]) {
class TypeAdapter(private val modules: Map[Class[_], TypeModule]) extends TypeLoader {

private val primitiveModule = new PrimitiveModule()

private val typeMap = new java.util.concurrent.ConcurrentHashMap[String, Type]()

protected[types] def saveType(name: String, t: Type): Unit = typeMap.put(name, t)

/**
* @param javaType
* @return
Expand Down Expand Up @@ -70,5 +74,11 @@ class TypeAdapter(private val modules: Map[Class[_], TypeModule]) {
module.fromJava(this, obj)
}
}

override def loadType(name: String): Option[Type] =
Option(typeMap.get(name))

override def removeType(name: String): Unit =
typeMap.remove(name)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ing.baker.types

trait TypeLoader {

protected[types] def removeType(name: String): Unit

protected[types] def saveType(name: String, t: Type): Unit

def loadType(name: String): Option[Type]
}

object TypeLoader {

val defaultTypeLoader: TypeLoader = Converters.defaultTypeConverter
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
package com.ing.baker.types.modules

import java.lang.reflect.Modifier

import com.ing.baker.types._
import org.objenesis.ObjenesisStd

import scala.util.{Failure, Success, Try}

class PojoModule extends TypeModule {

override def isApplicable(javaType: java.lang.reflect.Type): Boolean = true

override def readType(context: TypeAdapter, javaType: java.lang.reflect.Type): Type = {
val pojoClass = getBaseClass(javaType)
val fields = pojoClass.getDeclaredFields.toIndexedSeq.filterNot(f => f.isSynthetic || Modifier.isStatic(f.getModifiers))
val ingredients = fields.map(f => RecordField(f.getName, context.readType(f.getGenericType)))
RecordType(ingredients)
val className = pojoClass.getName

context.loadType(className) match {
case Some(t) => t
case _ =>
Try {
// we save the type as a reference type to avoid recursion
context.saveType(className, ReferenceType(context, className));

val fields = pojoClass.getDeclaredFields.toIndexedSeq.filterNot(f => f.isSynthetic || Modifier.isStatic(f.getModifiers))
val ingredients = fields.map(f => RecordField(f.getName, context.readType(f.getGenericType)))
val `type` = RecordType(ingredients)
context.saveType(className, `type`);
RecordType(ingredients)

} match {

case Failure(e) =>
context.removeType(className)
throw e
case Success(result) =>
context.saveType(className, result);
result
}
}
}

override def toJava(context: TypeAdapter, value: Value, javaType: java.lang.reflect.Type): Any = value match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ case class ComplexPOJOExample(simplePOJOExample: SimplePOJOExample,
string: String,
boolean: Boolean) {
}

case class RecursiveType(a: String,
b: Option[RecursiveType])
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import java.util
import java.util.Optional
import scala.annotation.nowarn
import scala.collection.JavaConverters._
import scala.reflect.runtime.universe

case class PersonCaseClass(name: String, age: Int)

Expand Down Expand Up @@ -233,6 +234,17 @@ class JavaModulesSpec extends AnyWordSpecLike with Matchers {
RecordField("boolean", types.Bool))

readJavaType[ComplexPOJOExample] shouldBe RecordType(complexPOJOExampleSeq)
readJavaType[ComplexPOJOExample] shouldBe RecordType(complexPOJOExampleSeq)
}

"be able to parse recursive types" in {

readJavaType[RecursiveType] shouldBe RecordType(
Seq(
RecordField("a", types.CharArray),
RecordField("b", OptionType(ReferenceType(defaultTypeConverter, "com.ing.baker.types.RecursiveType")))
)
)
}
}
}
Loading