Skip to content

Commit

Permalink
Merge pull request #209 from tofu-tf/newtypes
Browse files Browse the repository at this point in the history
Support estatico newtypes
  • Loading branch information
Odomontois authored Feb 16, 2021
2 parents 3dfca42 + 10c81e7 commit 72aaeb5
Show file tree
Hide file tree
Showing 22 changed files with 147 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/scala.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
scala: [2.12.12, 2.13.4]
scala: [2.12.13, 2.13.4]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ target/
.metals
.bloop
.vscode
metals.sbt
metals.sbt
.bsp
.metals
13 changes: 9 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name := "derevo"
import com.typesafe.sbt.SbtGit.git

val publishVersion = "0.11.6"
val publishVersion = "0.12.0"

val common = List(
scalaVersion := "2.13.4",
crossScalaVersions := List("2.12.12", "2.13.4"),
crossScalaVersions := List("2.12.13", "2.13.4"),
libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value % Provided,
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand All @@ -21,9 +21,14 @@ val common = List(
"-language:higherKinds",
"-Xfatal-warnings",
),
Test / scalacOptions ++= Vector(
"-language:implicitConversions",
),
libraryDependencies += "io.estatico" %% "newtype" % "0.4.4" % Test,
libraryDependencies += "org.scalameta" %% "munit" % Version.munit % "test",
libraryDependencies += "org.scalatest" %% "scalatest" % Version.scalaTest % "test",
scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, y)) if y == 11 => Seq("-Xexperimental")
case Some((2, y)) if y == 13 => Seq("-Ymacro-annotations")
case _ => Seq.empty[String]
}
Expand Down Expand Up @@ -76,7 +81,7 @@ lazy val core = project settings common settings (
s"""{"artifact": "$integrationOrg % ${integrationName}_2.13 % $integrationVersion"}""".stripMargin
)
Seq(intellijCompatFile)
}
},
)

lazy val cats = project dependsOn core settings common
Expand Down
3 changes: 2 additions & 1 deletion cats/src/main/scala/derevo/cats/eq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package derevo.cats
import cats.Eq
import magnolia.{CaseClass, Magnolia, SealedTrait}
import derevo.Derivation
import derevo.NewTypeDerivation

object eq extends Derivation[Eq] {
object eq extends Derivation[Eq] with NewTypeDerivation[Eq] {
type Typeclass[T] = Eq[T]

def combine[T](ctx: CaseClass[Eq, T]): Eq[T] = new Eq[T] {
Expand Down
11 changes: 9 additions & 2 deletions cats/src/main/scala/derevo/cats/monoid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package derevo.cats
import cats.Monoid
import magnolia.{CaseClass, Magnolia, SealedTrait}
import derevo.Derivation
import scala.annotation.implicitNotFound
import derevo.NewTypeDerivation

object monoid extends Derivation[Monoid] {
object monoid extends Derivation[Monoid] with NewTypeDerivation[Monoid] {
type Typeclass[T] = Monoid[T]

def combine[T](ctx: CaseClass[Monoid, T]): Monoid[T] = new Monoid[T] {
Expand All @@ -13,7 +15,12 @@ object monoid extends Derivation[Monoid] {
override def empty: T = ctx.construct(_.typeclass.empty)
}

def dispatch[T](ctx: SealedTrait[Monoid, T]): Monoid[T] = ???
def dispatch[T](ctx: SealedTrait[Monoid, T])(implicit absurd: MonoidSumAbsurd): Monoid[T] = absurd.mon

implicit def instance[T]: Monoid[T] = macro Magnolia.gen[T]
}

@implicitNotFound("Can not derive Monoids for sealed families")
abstract class MonoidSumAbsurd {
def mon[A]: Monoid[A]
}
3 changes: 2 additions & 1 deletion cats/src/main/scala/derevo/cats/ord.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package derevo.cats
import cats.Order
import magnolia.{CaseClass, Magnolia, SealedTrait}
import derevo.Derivation
import derevo.NewTypeDerivation

object order extends Derivation[Order] {
object order extends Derivation[Order] with NewTypeDerivation[Order] {
type Typeclass[T] = Order[T]

def combine[T](ctx: CaseClass[Order, T]): Order[T] = new Order[T] {
Expand Down
11 changes: 9 additions & 2 deletions cats/src/main/scala/derevo/cats/semigroup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ package derevo.cats
import cats.Semigroup
import magnolia.{CaseClass, Magnolia, SealedTrait}
import derevo.Derivation
import scala.annotation.implicitNotFound
import derevo.NewTypeDerivation

object semigroup extends Derivation[Semigroup] {
object semigroup extends Derivation[Semigroup] with NewTypeDerivation[Semigroup] {
type Typeclass[T] = Semigroup[T]

def combine[T](ctx: CaseClass[Semigroup, T]): Semigroup[T] = new Semigroup[T] {
override def combine(x: T, y: T): T =
ctx.construct(param => param.typeclass.combine(param.dereference(x), param.dereference(y)))
}

def dispatch[T](ctx: SealedTrait[Semigroup, T]): Semigroup[T] = ???
def dispatch[T](ctx: SealedTrait[Semigroup, T])(implicit absurd: SemigroupSumAbsurd): Semigroup[T] = absurd.sg
implicit def instance[T]: Semigroup[T] = macro Magnolia.gen[T]
}

@implicitNotFound("Can not derive Semigroups for sealed families")
abstract class SemigroupSumAbsurd {
def sg[A]: Semigroup[A]
}
2 changes: 1 addition & 1 deletion cats/src/main/scala/derevo/cats/show.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package cats
import _root_.cats.Show
import magnolia.{CaseClass, Magnolia, SealedTrait}

object show extends Derivation[Show] {
object show extends Derivation[Show] with NewTypeDerivation[Show] {
type Typeclass[T] = Show[T]

def combine[T](ctx: CaseClass[Show, T]): Show[T] = new Show[T] {
Expand Down
14 changes: 14 additions & 0 deletions cats/src/test/scala/derevo/cats/EqSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.Eq
import derevo.cats.{eq => eqv}
import derevo.derive
import org.scalatest.freespec.AnyFreeSpec
import io.estatico.newtype.macros.newtype

class EqSpec extends AnyFreeSpec {

Expand All @@ -19,6 +20,13 @@ class EqSpec extends AnyFreeSpec {
assert(Eq[Qux].neqv(Qux(1), Qux(-1)))
}

"through newtype casting" - {
import derevo.cats.EqSpec.Jankurpo

assert(Eq[Jankurpo].eqv(Jankurpo("a"), Jankurpo("a")))
assert(Eq[Jankurpo].neqv(Jankurpo("a"), Jankurpo("b")))
}

"through Eq.fromUniversalEquals" in {
@derive(eqv.universal)
case class Qux(a: Int)
Expand All @@ -29,3 +37,9 @@ class EqSpec extends AnyFreeSpec {
}
}
}

object EqSpec {
@derive(eqv)
@newtype
case class Jankurpo(a: String)
}
7 changes: 3 additions & 4 deletions circe/build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
moduleName := "derevo-circe"

libraryDependencies += "io.circe" %% "circe-core" % Version.circe
libraryDependencies += "io.circe" %% "circe-derivation" % Version.circeDerivation
libraryDependencies += "io.circe" %% "circe-parser" % Version.circe % "test"
libraryDependencies += "org.scalatest" %% "scalatest" % Version.scalaTest % "test"
libraryDependencies += "io.circe" %% "circe-core" % Version.circe
libraryDependencies += "io.circe" %% "circe-derivation" % Version.circeDerivation
libraryDependencies += "io.circe" %% "circe-parser" % Version.circe % "test"
7 changes: 4 additions & 3 deletions circe/src/main/scala/derevo/circe/circe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package derevo.circe
import derevo.{Derevo, Derivation, PolyDerivation, delegating}
import io.circe.derivation.renaming
import io.circe.{Codec, Decoder, Encoder}
import derevo.NewTypeDerivation

@delegating("io.circe.derivation.deriveDecoder")
object decoder extends Derivation[Decoder] {
object decoder extends Derivation[Decoder] with NewTypeDerivation[Decoder] {
def instance[A]: Decoder[A] = macro Derevo.delegate[Decoder, A]

/** @param arg1 naming function. For example io.circe.derivation.renaming.snakeCase
Expand All @@ -17,7 +18,7 @@ object decoder extends Derivation[Decoder] {
}

@delegating("io.circe.derivation.deriveEncoder")
object encoder extends PolyDerivation[Encoder, Encoder.AsObject] {
object encoder extends PolyDerivation[Encoder, Encoder.AsObject] with NewTypeDerivation[Encoder] {
def instance[A]: Encoder.AsObject[A] = macro Derevo.delegate[Encoder.AsObject, A]

/** @param arg1 naming function. For example io.circe.derivation.renaming.snakeCase
Expand All @@ -28,7 +29,7 @@ object encoder extends PolyDerivation[Encoder, Encoder.AsObject] {
}

@delegating("io.circe.derivation.deriveCodec")
object codec extends PolyDerivation[Codec, Codec.AsObject] {
object codec extends PolyDerivation[Codec, Codec.AsObject] with NewTypeDerivation[Codec] {
def instance[A]: Codec.AsObject[A] = macro Derevo.delegate[Codec.AsObject, A]

/** @param arg1 naming function. For example io.circe.derivation.renaming.snakeCase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package derevo.circe.magnolia
import derevo.{Derevo, Derivation, PolyDerivation, delegating}
import io.circe.magnolia.configured.Configuration
import io.circe.{Decoder, Encoder}
import derevo.NewTypeDerivation

@delegating("io.circe.magnolia.derivation.decoder.semiauto.deriveMagnoliaDecoder")
object decoder extends Derivation[Decoder] {
object decoder extends Derivation[Decoder] with NewTypeDerivation[Decoder] {
def instance[A]: Decoder[A] = macro Derevo.delegate[Decoder, A]
}

@delegating("io.circe.magnolia.derivation.encoder.semiauto.deriveMagnoliaEncoder")
object encoder extends Derivation[Encoder] {
object encoder extends Derivation[Encoder] with NewTypeDerivation[Encoder] {
def instance[A]: Encoder[A] = macro Derevo.delegate[Encoder, A]
}

Expand Down
3 changes: 2 additions & 1 deletion ciris/src/main/scala/derevo/ciris/ciris.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import magnolia.{CaseClass, Magnolia, SealedTrait}
import derevo.Derivation

import scala.language.experimental.macros
import derevo.NewTypeDerivation

object cirisDecoder extends Derivation[ConfigValueDecoder] {
object cirisDecoder extends Derivation[ConfigValueDecoder] with NewTypeDerivation[ConfigValueDecoder] {
type Typeclass[T] = ConfigValueDecoder[T]
type ErrorOrParams = Either[ConfigError, List[Any]]

Expand Down
27 changes: 23 additions & 4 deletions core/src/main/scala/derevo/Derevo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Derevo(val c: blackbox.Context) {
val IsDerivationK2 = isInstanceDef[DerivationK2[Dummy3]](1)
val IIH = weakTypeOf[InjectInstancesHere].typeSymbol

val EstaticoFQN = "io.estatico.newtype.macros.newtype"

def delegate[TC[_], I]: c.Expr[TC[I]] =
c.Expr(delegation(c.prefix.tree, None))

Expand Down Expand Up @@ -68,6 +70,16 @@ class Derevo(val c: blackbox.Context) {
maybeCall.fold(default)(call => call(method, args))
}

def isConstructionOf(name: String)(t: Tree): Boolean = t match {
case q"new $cls(...$_)" =>
val tts = c.typecheck(t, silent = true).symbol
tts.isMethod && {
val o = tts.asMethod.owner
o.isClass && o.asClass.fullName == name
}
case _ => false
}

def deriveMacro(annottees: Tree*): Tree = {
annottees match {
case Seq(obj: ModuleDef) =>
Expand Down Expand Up @@ -114,14 +126,20 @@ class Derevo(val c: blackbox.Context) {
pre ++ instances ++ post.drop(1)
}

private def instances(cls: ImplDef): List[Tree] =
private def instances(cls: ImplDef): List[Tree] = {
val newType = cls match {
case c: ClassDef if c.mods.annotations.exists(isConstructionOf(EstaticoFQN)) =>
c.impl.body.collectFirst { case q"$mods val $n :$t" if mods.hasFlag(Flag.CASEACCESSOR) => t }
case _ => None
}
c.prefix.tree match {
case q"new derive(..${instances})" =>
instances
.map(buildInstance(_, cls))
.map(buildInstance(_, cls, newType))
}
}

private def buildInstance(tree: Tree, impl: ImplDef): Tree = {
private def buildInstance(tree: Tree, impl: ImplDef, newType: Option[Tree]): Tree = {
val typRef = impl match {
case cls: ClassDef => tq"${impl.name.toTypeName}"
case obj: ModuleDef => tq"${obj.name}.type"
Expand All @@ -138,7 +156,8 @@ class Derevo(val c: blackbox.Context) {

case q"$obj" =>
val (name, from, to, drop) = nameAndTypes(obj)
(name, from, to, drop, q"$obj.instance")
val call = newType.fold(q"$obj.instance")(t => q"$obj.newtype[$t].instance")
(name, from, to, drop, call)
}

val tn = TermName(name)
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/scala/derevo/NewTypeRepr.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package derevo

/** Utilitary holder for the newtype instance derivation */
class NewTypeRepr[TC[_], R](private val repr: TC[R]) extends AnyVal {
def instance[A]: TC[A] = repr.asInstanceOf[TC[A]]
}

trait NewTypeDerivation[TC[_]] {
final def newtype[R](implicit repr: TC[R]): NewTypeRepr[TC, R] = new NewTypeRepr[TC, R](repr)
}
31 changes: 31 additions & 0 deletions core/src/test/scala/derevo/KyonSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package derevo

import io.estatico.newtype.macros.newtype
import scala.language.implicitConversions
import scala.reflect.{ClassTag, classTag}
import org.scalatest.flatspec.AnyFlatSpec

class Kyon[A](val cls: Any)

object Kyon extends Derivation[Kyon] {
def instance[A](implicit cls: ClassTag[A]): Kyon[A] = new Kyon(cls)
def newtype[Repr]: Newtype[Repr] = new Newtype[Repr]

class Newtype[Repr] {
def instance[A](implicit cls: ClassTag[Repr]): Kyon[A] = new Kyon(cls)
}
}

class KyonSuite extends AnyFlatSpec {
import KyonSuite._

"newtype derivation" should "implicitly receive classtag" in {
assert(implicitly[Kyon[Datus[Unit]]].cls === classTag[String])
}
}

object KyonSuite {
@derive(Kyon)
@newtype
case class Datus[@phantom A](x: String)
}
4 changes: 4 additions & 0 deletions project/Version.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ object Version {
val typedSchema = "0.14.2"

val scalaCheck = "1.15.2"

val estatico = "0.4.4"

val munit = "0.7.21"
}
9 changes: 5 additions & 4 deletions pureconfig/src/main/scala/derevo/pureconfig/pureconfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ package derevo.pureconfig

import derevo.{Derevo, Derivation, delegating}
import pureconfig.{ConfigReader, ConfigWriter}
import derevo.NewTypeDerivation

@delegating("pureconfig.generic.semiauto.deriveReader")
object pureconfigReader extends Derivation[ConfigReader] {
object pureconfigReader extends Derivation[ConfigReader] with NewTypeDerivation[ConfigReader] {
def instance[A]: ConfigReader[A] = macro Derevo.delegate[ConfigReader, A]
}

@delegating("pureconfig.generic.semiauto.deriveWriter")
object pureconfigWriter extends Derivation[ConfigWriter] {
object pureconfigWriter extends Derivation[ConfigWriter] with NewTypeDerivation[ConfigWriter] {
def instance[A]: ConfigWriter[A] = macro Derevo.delegate[ConfigWriter, A]
}

@delegating("pureconfig.module.magnolia.semiauto.reader.deriveReader")
object config extends Derivation[ConfigReader] {
object config extends Derivation[ConfigReader] with NewTypeDerivation[ConfigReader] {
def instance[A]: ConfigReader[A] = macro Derevo.delegate[ConfigReader, A]
}

@delegating("pureconfig.module.magnolia.semiauto.writer.deriveWriter")
object configWriter extends Derivation[ConfigWriter] {
object configWriter extends Derivation[ConfigWriter] with NewTypeDerivation[ConfigWriter] {
def instance[A]: ConfigWriter[A] = macro Derevo.delegate[ConfigWriter, A]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ package object reactivemongo {
type BsonValueWriter[A] = BSONWriter[A, _ <: BSONValue]

@delegating("reactivemongo.bson.Macros.writer")
object bsonDocumentWriter extends PolyDerivation[BsonValueWriter, BSONDocumentWriter] {
object bsonDocumentWriter
extends PolyDerivation[BsonValueWriter, BSONDocumentWriter] with NewTypeDerivation[BSONDocumentWriter] {
def instance[A]: BSONDocumentWriter[A] = macro Derevo.delegate[BSONDocumentWriter, A]
}

@delegating("reactivemongo.bson.Macros.reader")
object bsonDocumentReader extends PolyDerivation[BsonValueReader, BSONDocumentReader] {
object bsonDocumentReader
extends PolyDerivation[BsonValueReader, BSONDocumentReader] with NewTypeDerivation[BSONDocumentReader] {
def instance[A]: BSONDocumentReader[A] = macro Derevo.delegate[BSONDocumentReader, A]
}
}
Loading

0 comments on commit 72aaeb5

Please sign in to comment.