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

Upgrade magnolia and restore defaults #791

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ project/plugins/project/
# Scala-IDE specific
.scala_dependencies
.worksheet
.metals
.bloop

credentials.sbt
.idea
Expand Down
149 changes: 77 additions & 72 deletions avro4s-core/src/main/scala/com/sksamuel/avro4s/CustomDefaults.scala
Original file line number Diff line number Diff line change
@@ -1,72 +1,77 @@
//package com.sksamuel.avro4s
//
//import java.time.Instant
//
//import magnolia.{SealedTrait, Subtype}
//import org.json4s.native.JsonMethods.parse
//import org.json4s.native.Serialization.write
//import org.apache.avro.Schema
//import org.apache.avro.Schema.Type
//import org.json4s.DefaultFormats
//
//import scala.collection.JavaConverters._
//
//sealed trait CustomDefault
//case class CustomUnionDefault(className: String, values: java.util.Map[String, Any]) extends CustomDefault
//case class CustomUnionWithEnumDefault(parentName: String, default: String, value: String) extends CustomDefault
//case class CustomEnumDefault(value: String) extends CustomDefault
//
//object CustomDefaults {
//
// implicit val formats = DefaultFormats
//
// def customScalaEnumDefault(value: Any) = CustomEnumDefault(value.toString)
//
// def customDefault(p: Product, schema: Schema): CustomDefault =
// if(isEnum(p, schema.getType))
// CustomEnumDefault(trimmedClassName(p))
// else {
// if(isUnionOfEnum(schema)) {
// val enumType = schema.getTypes.asScala.filter(_.getType == Schema.Type.ENUM).head
// CustomUnionWithEnumDefault(enumType.getName, trimmedClassName(p), p.toString)
// } else
// CustomUnionDefault(trimmedClassName(p), parse(write(p)).extract[Map[String, Any]].map {
// case (name, b: BigInt) if b.isValidInt => name -> b.intValue
// case (name, b: BigInt) if b.isValidLong => name -> b.longValue
// case (name, z) if schema.getType == Type.UNION => name ->
// schema.getTypes.asScala.find(_.getName == trimmedClassName(p)).map(_.getField(name).schema())
// .map(DefaultResolver(z, _)).getOrElse(z)
// case (name, z) => name -> DefaultResolver(z, schema.getField(name).schema())
//
// }.asJava)
// }
//
// def isUnionOfEnum(schema: Schema) = schema.getType == Schema.Type.UNION && schema.getTypes.asScala.map(_.getType).contains(Schema.Type.ENUM)
//
// def sealedTraitEnumDefaultValue[T](ctx: SealedTrait[SchemaFor, T]) = {
// val defaultExtractor = new AnnotationExtractors(ctx.annotations)
// defaultExtractor.enumDefault.flatMap { default =>
// ctx.subtypes.flatMap { st: Subtype[SchemaFor, T] =>
// if(st.typeName.short == default.toString)
// Option(st.typeName.short)
// else
// None
// }.headOption
// }
// }
//
// def isScalaEnumeration(value: Any) = value.getClass.getCanonicalName == "scala.Enumeration.Val"
//
// def customInstantDefault(instant: Instant): java.lang.Long = instant match {
// case Instant.MAX => Instant.ofEpochMilli(Long.MaxValue).toEpochMilli()
// case Instant.MIN => Instant.ofEpochMilli(Long.MinValue).toEpochMilli()
// case _ => instant.toEpochMilli()
// }
//
// private def isEnum(product: Product, schemaType: Schema.Type) =
// product.productArity == 0 && schemaType == Schema.Type.ENUM
//
// private def trimmedClassName(p: Product) = trimDollar(p.getClass.getSimpleName)
//
// private def trimDollar(s: String) = if(s.endsWith("$")) s.dropRight(1) else s
//}
package com.sksamuel.avro4s

import java.time.Instant

import magnolia1.SealedTrait
import org.json4s.native.JsonMethods.parse
import org.json4s.native.Serialization.write
import org.apache.avro.Schema
import org.apache.avro.Schema.Type
import org.json4s.DefaultFormats
import org.json4s.jvalue2extractable
import scala.reflect.Enum
import scala.collection.JavaConverters._
import com.sksamuel.avro4s.typeutils.Annotations

sealed trait CustomDefault
case class CustomUnionDefault(className: String, values: java.util.Map[String, Any]) extends CustomDefault
case class CustomUnionWithEnumDefault(parentName: String, default: String, value: String) extends CustomDefault
case class CustomEnumDefault(value: String) extends CustomDefault

object CustomDefaults {

given formats: DefaultFormats = DefaultFormats

def customScalaEnumDefault(value: Any) = CustomEnumDefault(value.toString)

def customDefault(p: Product, schema: Schema): CustomDefault =
if(isEnum(p, schema.getType))
// CustomEnumDefault(trimmedClassName(p))
CustomEnumDefault(p.toString())
else {
if(isUnionOfEnum(schema)) {
val enumType = schema.getTypes.asScala.filter(_.getType == Schema.Type.ENUM).head
CustomUnionWithEnumDefault(enumType.getName, trimmedClassName(p), p.toString)
} else
CustomUnionDefault(trimmedClassName(p), parse(write(p)).extract[Map[String, Any]].map {
case (name, b: BigInt) if b.isValidInt => name -> b.intValue
case (name, b: BigInt) if b.isValidLong => name -> b.longValue
case (name, z) if schema.getType == Type.UNION => name ->
schema.getTypes.asScala.find(_.getName == trimmedClassName(p)).map(_.getField(name).schema())
.map(DefaultResolver(z, _)).getOrElse(z)
case (name, z) => name -> DefaultResolver(z, schema.getField(name).schema())

}.asJava)
}

def isUnionOfEnum(schema: Schema) =
val types = schema.getTypes.asScala.map(_.getType)
schema.getType == Schema.Type.UNION && schema.getTypes.asScala.map(_.getType).contains(Schema.Type.ENUM)

def sealedTraitEnumDefaultValue[T](ctx: SealedTrait[SchemaFor, T]): Option[String] =
val defaultExtractor = Annotations(ctx.annotations)
defaultExtractor.enumDefault.flatMap { default =>
ctx.subtypes.flatMap { (st: SealedTrait.Subtype[SchemaFor, T, _]) =>
if st.typeInfo.short == default.toString then
Some(st.typeInfo.short)
else
None
}.headOption
}

def isScalaEnumeration(value: Any) =
value.isInstanceOf[Enum]

def customInstantDefault(instant: Instant): java.lang.Long = instant match {
case Instant.MAX => Instant.ofEpochMilli(Long.MaxValue).toEpochMilli()
case Instant.MIN => Instant.ofEpochMilli(Long.MinValue).toEpochMilli()
case _ => instant.toEpochMilli()
}

private def isEnum(product: Product, schemaType: Schema.Type) =
product.productArity == 0 && schemaType == Schema.Type.ENUM

private def trimmedClassName(p: Product) = trimDollar(p.getClass.getSimpleName)

private def trimDollar(s: String) = if(s.endsWith("$")) s.dropRight(1) else s
}
109 changes: 56 additions & 53 deletions avro4s-core/src/main/scala/com/sksamuel/avro4s/DefaultResolver.scala
Original file line number Diff line number Diff line change
@@ -1,53 +1,56 @@
//package com.sksamuel.avro4s
//
//import java.nio.ByteBuffer
//import java.time.Instant
//import java.util.UUID
//
//import org.apache.avro.LogicalTypes.Decimal
//import org.apache.avro.generic.{GenericEnumSymbol, GenericFixed}
//import org.apache.avro.util.Utf8
//import org.apache.avro.{Conversions, Schema}
//import CustomDefaults._
//import scala.collection.JavaConverters._
//
///**
// * When we set a default on an avro field, the type must match
// * the schema definition. For example, if our field has a schema
// * of type UUID, then the default must be a String, or for a schema
// * of Long, then the type must be a java Long and not a Scala long.
// *
// * This class will accept a scala value and convert it into a type
// * suitable for Avro and the provided schema.
// */
//object DefaultResolver {
//
// def apply(value: Any, schema: Schema): AnyRef = value match {
// case Some(x) => apply(x, schema)
// case u: Utf8 => u.toString
// case uuid: UUID => uuid.toString
// case enum: GenericEnumSymbol[_] => enum.toString
// case instant: Instant => customInstantDefault(instant)
// case fixed: GenericFixed => fixed.bytes()
// case bd: BigDecimal => bd.toString()
// case byteBuffer: ByteBuffer if schema.getLogicalType.isInstanceOf[Decimal] =>
// val decimalConversion = new Conversions.DecimalConversion
// val bd = decimalConversion.fromBytes(byteBuffer, schema, schema.getLogicalType)
// java.lang.Double.valueOf(bd.doubleValue)
// case byteBuffer: ByteBuffer => byteBuffer.array()
// case x: scala.Long => java.lang.Long.valueOf(x)
// case x: scala.Boolean => java.lang.Boolean.valueOf(x)
// case x: scala.Int => java.lang.Integer.valueOf(x)
// case x: scala.Double => java.lang.Double.valueOf(x)
// case x: scala.Float => java.lang.Float.valueOf(x)
// case x: Map[_,_] => x.asJava
// case x: Seq[_] => x.asJava
// case x: Set[_] => x.asJava
// case shapeless.Inl(x) => apply(x, schema)
// case p: Product => customDefault(p, schema)
// case v if isScalaEnumeration(v) => customScalaEnumDefault(value)
// case _ =>
// value.asInstanceOf[AnyRef]
// }
//
//}
package com.sksamuel.avro4s

import java.nio.ByteBuffer
import java.time.Instant
import java.util.UUID

import org.apache.avro.LogicalTypes.Decimal
import org.apache.avro.generic.{GenericEnumSymbol, GenericFixed}
import org.apache.avro.util.Utf8
import org.apache.avro.{Conversions, Schema}
import CustomDefaults._
import scala.collection.JavaConverters._

/**
* When we set a default on an avro field, the type must match
* the schema definition. For example, if our field has a schema
* of type UUID, then the default must be a String, or for a schema
* of Long, then the type must be a java Long and not a Scala long.
*
* This class will accept a scala value and convert it into a type
* suitable for Avro and the provided schema.
*/
object DefaultResolver {

def apply(value: Any, schema: Schema): AnyRef = value match {
case Some(x) => apply(x, schema)
// case Some(x) => apply(x, schema.getTypes.asScala.filterNot(_.getType == Schema.Type.NULL).head)
case u: Utf8 => u.toString
case uuid: UUID => uuid.toString
case enumSymbol: GenericEnumSymbol[_] => enumSymbol.toString
case instant: Instant => customInstantDefault(instant)
case fixed: GenericFixed => fixed.bytes()
case bd: BigDecimal => bd.toString()
case byteBuffer: ByteBuffer if schema.getLogicalType.isInstanceOf[Decimal] =>
val decimalConversion = new Conversions.DecimalConversion
val bd = decimalConversion.fromBytes(byteBuffer, schema, schema.getLogicalType)
java.lang.Double.valueOf(bd.doubleValue)
case byteBuffer: ByteBuffer => byteBuffer.array()
case x: scala.Long => java.lang.Long.valueOf(x)
case x: scala.Boolean => java.lang.Boolean.valueOf(x)
case x: scala.Int => java.lang.Integer.valueOf(x)
case x: scala.Double => java.lang.Double.valueOf(x)
case x: scala.Float => java.lang.Float.valueOf(x)
case x: Map[_,_] => x.asJava
case x: Seq[_] => x.asJava
case x: Set[_] => x.asJava
case p: Product =>
customDefault(p, schema)
// todo figure out if this is still needed
case v if isScalaEnumeration(v) =>
customScalaEnumDefault(value)
case _ =>
value.asInstanceOf[AnyRef]
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.sksamuel.avro4s.avroutils

import com.sksamuel.avro4s.{Avro4sConfigurationException, FieldMapper}
import com.sksamuel.avro4s.{Avro4sConfigurationException, CustomUnionDefault, CustomUnionWithEnumDefault, FieldMapper}
import org.apache.avro.generic.GenericData
import org.apache.avro.util.Utf8
import org.apache.avro.{JsonProperties, Schema, SchemaBuilder}
Expand Down Expand Up @@ -124,9 +124,9 @@ object SchemaHelper {

val (first, rest) = schema.getTypes.asScala.partition { t =>
defaultType match {
// case CustomUnionDefault(name, _) => name == t.getName
// case CustomUnionWithEnumDefault(name, default, _) =>
// name == t.getName
case CustomUnionDefault(name, _) => name == t.getName
case CustomUnionWithEnumDefault(name, default, _) =>
name == t.getName
case _ => t.getType == defaultType
}
}
Expand Down
Loading