plokhotnyuk / jsoniter-scala

Scala macros for compile-time generation of safe and ultra-fast JSON codecs + circe booster
MIT License
738 stars 98 forks source link

Global defaults for codecs #999

Open mortenhornbech opened 1 year ago

mortenhornbech commented 1 year ago

We have a number of configuration defaults we would like to reuse for multiple codecs, but when we factor them out into a variable we get the following compile error:

Cannot evaluate a parameter of the 'make' macro call for type 'String'. It should not depend on code from the same compilation module where the 'make' macro is called. Use a separated submodule of the project to compile all such dependencies before their usage for generation of codecs. Cause: [error] java.lang.reflect.InvocationTargetException [error] implicit val stringCodec: JsonValueCodec[String] = JsonCodecMaker.make(config)

What is more specifically required here. Does it need to be a completely seperate dependency where we store the config, or can we change the structure of the code somehow?

We could of course copy-paste the same configuration around, but that is difficult to maintain.

plokhotnyuk commented 1 year ago

You cannot reuse config as value or method call, but there is a couple of options:

1) Use preconfigured make... calls without parameters 2) Use derives keyword from Scala 3 like here 3) Use some other macro that can generate the configuration expression in the place of the config parameter

Also, you can easily ask me for adding an additional make... call preconfigured for your needs, especially if your business would like to donate a bit.

mortenhornbech commented 1 year ago

Thanks for quick reply! Generally we would like some more defensive defaults. For example we have had to make a lot of changes to javascript client code expecting empty collections and default values to be included. Maybe a parameter to specify preference for interoperability over json-size make sense. We are open to donations, but we need changes to be available on 2.13.5.x. Impressive framework you have build.

plokhotnyuk commented 1 year ago

Unfortunately, 2.13.5.x is closed for adding new methods, only backward and forward binary compatible patches are applicable.

It could be possible to bring Java 8 compatible code to mainline, but need to upgrade sbt-multi-release-jar plugin to use Java 11+ and to be able to work with a multi-platform build configuration.

I suppose that you use Scala 2 now, so please check if the 3rd option with a macro that generates the config parameter works for you:

Config generation macro

import com.github.plokhotnyuk.jsoniter_scala.macros._
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context

object DefaultCodecMakerConfig {
  def gen: CodecMakerConfig = macro genImpl

  def genImpl(c: Context): c.universe.Tree = {
    import c.universe._

    q"""_root_.com.github.plokhotnyuk.jsoniter_scala.macros.CodecMakerConfig
          .withTransientEmpty(false)
          .withTransientDefault(false)
          .withTransientNone(false)"""
  }
}

Usage of config generation macro

import com.github.plokhotnyuk.jsoniter_scala.macros._
import com.github.plokhotnyuk.jsoniter_scala.core._

case class DefaultCodecMakerConfigTest(d: String = "VVV", o: Option[Boolean] = None, s: Seq[Int] = Nil)

implicit val codec: JsonValueCodec[DefaultCodecMakerConfig] = JsonCodecMaker.make(DefaultCodecMakerConfig.gen)

assert(writeToString(DefaultCodecMakerConfigTest()) == """{"d":"VVV","o":null,"s":[]}""")

Beware that the generating macro should be compiled before its usage in a separated module.

mortenhornbech commented 1 year ago

We use scala 2 yes. I will look into your suggestion later this week. Thanks!

plokhotnyuk commented 1 year ago

@mortenhornbech Have you had a chance to test the proposed approach of config generation macro?

mortenhornbech commented 1 year ago

@plokhotnyuk Sorry, got caught up in urgent stuff. An issue is that our codecs already are located in our base module, so if I read your suggestion correctly, we would have to create a new base-base module for the sole purpose of this macro. And in that case do I then even need the macro? The original error message seems to suggest that I would be able to reference a shared variable in a base module.

plokhotnyuk commented 1 year ago

Yes, the macro in a separated module is only the option in Scala 2. For Scala 3 the macro is still required but it could be defined in the same module.

In the terse error message I tried to explain that name mapping functions which could be used for compile-time configuration with Scala 2 should be pure functions that do not depend even on static members of the context of definition. For Scala 3 limitations are harder, because built-in interpreter of name mapping functions in compile-time allows only simplest implementation of them.