softwaremill / magnolia

Easy, fast, transparent generic derivation of typeclass instances
https://softwaremill.com/open-source/
Apache License 2.0
765 stars 116 forks source link

Null typeclass for Param in automatic derivation #402

Open wsargent opened 2 years ago

wsargent commented 2 years ago

I have a failing spec in my project that is coming from a null typeclass, and I suspect this is a bug in Magnolia

[info] - should derive a case class *** FAILED ***
[info]   java.lang.NullPointerException: type class is null!
[info]   at java.base/java.util.Objects.requireNonNull(Objects.java:246)
[info]   at com.tersesystems.echopraxia.plusscala.api.Derivation.$anonfun$joinCaseClass$2(Derivation.scala:75)
[info]   at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:286)
[info]   at scala.collection.IndexedSeqOptimized.foreach(IndexedSeqOptimized.scala:36)
[info]   at scala.collection.IndexedSeqOptimized.foreach$(IndexedSeqOptimized.scala:33)
[info]   at scala.collection.mutable.WrappedArray.foreach(WrappedArray.scala:38)
[info]   at scala.collection.TraversableLike.map(TraversableLike.scala:286)
[info]   at scala.collection.TraversableLike.map$(TraversableLike.scala:279)
[info]   at scala.collection.AbstractTraversable.map(Traversable.scala:108)
[info]   at com.tersesystems.echopraxia.plusscala.api.Derivation.com$tersesystems$echopraxia$plusscala$api$Derivation$$$anonfun$joinCaseClass$1(Derivation.scala:72)

Here's the code for it:

sealed trait Derivation extends ValueTypeClasses {
  type Typeclass[T] = ToValue[T]
  type CaseClass[T]   = magnolia1.CaseClass[Typeclass, T]
  type SealedTrait[T] = magnolia1.SealedTrait[Typeclass, T]

  final def join[T](ctx: CaseClass[T]): Typeclass[T] = {
    if (ctx.isValueClass) {
      joinValueClass(ctx)
    } else if (ctx.isObject) {
      joinCaseObject(ctx)
    } else {
      joinCaseClass(ctx)
    }
  }

  // this is a regular case class
  protected def joinCaseClass[T](ctx: CaseClass[T]): Typeclass[T] = { obj =>
    val typeInfo = Field.keyValue("@type", ToValue(ctx.typeName.full))
    val fields: Seq[Field] = ctx.parameters.map { p =>
      val name: String = p.label
      val attribute = p.dereference(obj)
      val typeclassInstance = Objects.requireNonNull(p.typeclass, "type class is null!")
      val value: Value[_] = typeclassInstance.toValue(attribute)
      Field.keyValue(name, value)
    }
    ToObjectValue(typeInfo +: fields)
  }

  // this is a case object, we can't do anything with it.
  protected def joinCaseObject[T](ctx: CaseClass[T]): Typeclass[T] = {
    // ctx has no parameters, so we're better off just passing it straight through.
    value => Value.string(value.toString)
  }

  // this is a value class aka AnyVal, we should pass it through.
  protected def joinValueClass[T](ctx: CaseClass[T]): Typeclass[T] = {
    val param = ctx.parameters.head
    value => param.typeclass.toValue(param.dereference(value))
  }

  // this is a sealed trait
  def split[T](ctx: SealedTrait[T]): Typeclass[T] = (value: T) => {
    ctx.split(value) { sub =>
      sub.typeclass.toValue(sub.cast(value))
    }
  }
}

trait SemiAutoDerivation extends Derivation {
  final def gen[T]: Typeclass[T] = macro Magnolia.gen[T]
}

The PR at https://github.com/tersesystems/echopraxia-plusscala/pull/1#discussion_r884392808

This is taken directly from https://github.com/softwaremill/magnolia/blob/scala2/examples/src/main/scala/magnolia1/examples/print.scala#L22 so I don't think this should be possible.

wsargent commented 2 years ago

I've put together an isolated test case https://github.com/wsargent/magnolia-issue-402

wsargent commented 2 years ago

It looks like a simple workaround is to define generated implicits using implicit lazy val fooToValue = gen[Foo] and that will resolve the NPE (presumably because by then the instant implicit is in scope)