tofu-tf / derevo

Multiple instance derivations inside single macro annotation

Version Matrix

derevo

Build & Release Maven Central Scala Steward badge

Multiple instance derivations inside a single macro annotation.

Basic Usage

If a library has support for derevo, you can simply write

@derive(SomeCaseClass, AnotherCaseClass, fooDerivation, barDerivation(param1, param2))
case class Foo(...)

where SomeCaseClass, AnotherCaseClass, fooDerivation, barDerivation are some objects, extending one of the InstanceDef traits (see module source).

For every element of the @derive macro, there will be generated implicit val or implicit def (when Foo has type parameters) providing the corresponding typeclass. For simple type derivation, if Foo has type parameters, the instance will require an instance of the same or specified other typeclass for proper derivation. Example:

@derive(encoder)
case class Foo[@phantom A, B](...)

// `derive` generates:
implicit def encoder$macro$1[A, B: Encoder]: Encoder.AsObject[Foo[A, B]] = ...

Foo can also be a newtype in form of estatico or supertagged libraries.

If you have problems with the initialization order you can optionally put the insertInstancesHere() call to the body of your companion object to specify the place where implicit vals should be inserted.

Making your own derivation.

First, extend the object (the companion object for your type is the best option) from one of the InstanceDef traits.

Then implement the instance method for your object so it could derive the corresponding instance. Example:

trait TypeClass[A] {
...
}

object TypeClass extends Derivaton[TypeClass] {
  def instance[A]: TypeClass[A] = ...
}

Also, you can define additional methods def apply(...) or def foo(...) in your derivation object. This will allow instance creation as @derive(TypeClass(...)) or @derive(TypeClass.foo(...)).

To support newtype derivation, extend your object with (NewtypeDerivation)[https://github.com/tofu-tf/derevo/blob/master/core/src/main/scala/derevo/NewTypeRepr.scala#L8]. Alternatively, your object may have the newtype[R] method with a single type parameter that receives the underlying type and returns another object that has the instance method that works as described earlier.

Sometimes, the required constraint may differ from the provided typeclass, e.g. circe.Encoder which requires Encoder for each field but provides Encoder.AsObject for the target case class. In this case you should extend SpecificDerivation instead, like object foo extends SpecificDerivation[FromTc, ToTc, NT], where

  • FromTc[A] - typeclass that will be required for non-phantom type parameters
  • ToTc[A] - normal typeclass provided by the instance
  • NT[A] - type class that will be forwarded by the newtype derivation

Expression table

Derivation line Translation
@derive(...foo, ...) implicit val foo$macro: Foo[A] = foo.instance
@derive(...foo(bar)) implicit val foo$macro : Foo[A] = foo(bar)
@derive(...foo.quux(bar)) implicit val foo$macro : Foo[A] = foo.quux(bar)
@derive(...foo, ...) when A is a newtype over B implicit val foo$macro: Foo[A] = foo.newtype[B].instance

Delegating to other macros

Derevo supports macro delegation when the corresponding macro function is provided by another library.

This works by adding the @delegating annotation to your delegator object:

@delegating("full.qualified.method.path")

Then call macro Derevo.delegate, macro Derevo.delegateParams, macro Derevo.delegateParams2 or macro Derevo.delegateParams3 in the corresponding methods. An example can be found here.

Installation

For Scala 2.12 and older:

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)

For Scala 2.13:

scalacOptions += "-Ymacro-annotations"

Supported integrations

Cats

libraryDependencies += "tf.tofu" %% "derevo-cats" % "latest version in badge"
import derevo.derive
import derevo.cats.{eqv, show, order, monoid}

import cats.Monoid
import cats.instances.string._
import cats.instances.int._
import cats.syntax.show._
import cats.syntax.order._
import cats.syntax.semigroup._
import cats.syntax.monoid._

@derive(eqv, show, order, monoid)
case class Foo(bar: String, baz: Int)
@derive(eqv.universal)
case class Bar(x: Int)

assert(Foo("111", 222) === Foo("111", 222))
assert(Bar(1) === Bar(1))
assert(show"${Foo("111", 222)}" === "Foo{bar=111,baz=222}")
assert((Foo("111", 222) compare Foo("222", 333)) == -1)
assert((Foo("1", 1) |+| Foo("2", 2)) == Foo("12", 3))
assert(Monoid[Foo].empty == Foo("", 0))

Cats Tagless

libraryDependencies += "tf.tofu" %% "derevo-cats-tagless" % "latest version in badge"
import derevo.derive
import derevo.tagless.{functor, flatMap, invariant, contravariant, functorK, invariantK, semigroupalK, applyK}

// TODO

Tethys

libraryDependencies += "tf.tofu" %% "derevo-tethys" % "latest version in badge"
import derevo.derive
import derevo.tethys.{tethysReader, tethysWriter}

import tethys._
import tethys.derivation.builder.{FieldStyle, WriterDerivationConfig}
import tethys.jackson._

@derive(
  tethysReader,
  tethysWriter(WriterDerivationConfig.withFieldStyle(FieldStyle.lowerSnakecase))
)
final case class Bar(stringName: String, integerAge: Int)

assert(Bar("Cotique", 27).asJson == """{"string_name":"Cotique","integer_age":27}""")
assert("""{"stringName":"Elya","integerAge":32}""".jsonAs[Bar] == Right(Bar("Elya", 32)))

Circe

libraryDependencies += "tf.tofu" %% "derevo-circe" % "latest version in badge"
import derevo.derive
import derevo.circe.{decoder, encoder}

import io.circe._
import io.circe.syntax._
import io.circe.parser._

@derive(decoder, encoder)
final case class Bar(stringName: String, integerAge: Int)

assert(Bar("KKK", 22).asJson.printWith(Printer.noSpaces) == """{"stringName":"KKK","integerAge":22}""")
assert(parse("""{"stringName":"WWW","integerAge":20}""").flatMap(_.as[Bar]) == Right(Bar("WWW", 20)))

Circe Magnolia

libraryDependencies += "tf.tofu" %% "derevo-circe-magnolia" % "latest version in badge"
import derevo.derive
import derevo.circe.magnolia.{decoder, encoder}

import io.circe._
import io.circe.syntax._
import io.circe.parser._

@derive(decoder, encoder)
final case class Bar(stringName: String, integerAge: Int)

assert(Bar("KKK", 22).asJson.printWith(Printer.noSpaces) == """{"stringName":"KKK","integerAge":22}""")
assert(parse("""{"stringName":"WWW","integerAge":20}""").flatMap(_.as[Bar]) == Right(Bar("WWW", 20)))

To change default io.circe.magnolia.configured.Configuration:

import derevo.derive
import derevo.circe.magnolia.{customizableDecoder, customizableEncoder}

import io.circe._
import io.circe.syntax._
import io.circe.parser._

@derive(customizableEncoder, customizableDecoder)
sealed trait SealedTrait

object SealedTrait {
  implicit val configuration:Configuration = Configuration.default.withDiscriminator("type")

  @derive(encoder, decoder)
  case class Bar(bar: Int) extends SealedTrait

  @derive(encoder, decoder)
  case class Baz(baz: String) extends SealedTrait
}

Ciris + HOCON

libraryDependencies += "tf.tofu" %% "derevo-ciris" % "latest version in badge"
import derevo.derive
import derevo.ciris.cirisDecoder

import com.typesafe.config.ConfigFactory
import ciris.hocon._
import ciris.hocon.instances._

@derive(cirisDecoder)
case class DataConfig(name: String, addresses: List[String], mapping: Map[String, Int])

val source = hoconSource[DataConfig](
    ConfigFactory.parseString(
    """
      |data {
      |  name = AAA
      |  addresses = [home, work, pub]
      |  mapping.until = 1
      |  mapping.from  = 2
      |  mapping.to    = 3
      |}
      """.stripMargin
    ),
    "data"
)

// Load in F[_] context, cats.effect.IO just for example
import cats.effect.IO
import scala.concurrent.ExecutionContext.global

implicit val cs = IO.contextShift(global)

assert(source.load[IO].unsafeRunSync() == DataConfig("AAA", List("pub", "home", "work"), Map("until" -> 1, "from" -> 2, "to" -> 3)))

PureConfig

libraryDependencies += "tf.tofu" %% "derevo-pureconfig" % "latest version in badge"

or

libraryDependencies += "tf.tofu" %% "derevo-pureconfig-legacy" % "latest version in badge"
                                    // depends on pureconfig 0.14.1, doesn't force you to switch to CE3
import derevo.derive
import derevo.pureconfig.{pureconfigReader, pureconfigWriter}

import com.typesafe.config.ConfigFactory
import pureconfig._
import pureconfig.syntax._

@derive(pureconfigReader, pureconfigWriter)
case class DataConfig(name: String, addresses: List[String], mapping: Map[String, Int])

val raw = ConfigFactory
  .parseString(
    """
      |{
      |  name = AAA
      |  addresses = [home, work, pub]
      |  mapping.until = 1
      |  mapping.from  = 2
      |  mapping.to    = 3
      |}
        """.stripMargin
  )

val parsed = ConfigSource.fromConfig(raw).load[DataConfig]

assert(parsed == Right(DataConfig("AAA", List("home", "work", "pub"), Map("until" -> 1, "from" -> 2, "to" -> 3))))
assert(parsed.map(_.toConfig.atPath("data").getConfig("data")) == Right(raw))

RMongo

libraryDependencies += "tf.tofu" %% "derevo-rmongo" % "latest version in badge"
import derevo.derive
import derevo.reactivemongo.{bsonDocumentReader, bsonDocumentWriter}

// TODO

Scalacheck

libraryDependencies += "tf.tofu" %% "derevo-scalacheck" % "latest version in badge" % Test
import derevo.derive
import org.scalacheck.Arbitrary
import derevo.scalacheck.arbitrary

// for existing classes
sealed trait Bear
case class Beer(b: String, t: Boolean) extends Bear
case class Gear(g: Int) extends Bear

println(arbitrary.instance[Bear].arbitrary.sample)

// for classes in tests
@derive(arbitrary)
case class Test(x: String, y: List[Int])

println(implicitly[Arbitrary[Test]].arbitrary.sample)