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

Specify derived typeclass for newtypes in PolyDerivation #222

Merged
merged 3 commits into from
Feb 19, 2021
Merged
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
56 changes: 32 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# derevo

[![Build & Release](https://github.com/tofu-tf/derevo/workflows/Scala%20CI/badge.svg)](https://github.com/tofu-tf/derevo/actions?query=workflow%3A%22Scala+CI%22)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/tf.tofu/derevo-core_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/tf.tofu/derevo-core_2.13)
[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)


Multiple instance derivations inside a single macro annotation.


## Basic Usage
If a library has support for `derevo`, you can simply write
```scala
Expand All @@ -9,8 +16,8 @@ case class Foo(...)
```
where `SomeCaseClass`, `AnotherCaseClass`, `fooDerivation`, `barDerivation` are some objects, extending one of the `InstanceDef` traits ([see module source](https://github.com/tofu-tf/derevo/blob/supertagged/core/src/main/scala/derevo/package.scala#L13:L21)).

For every element of `@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 another typeclass for proper derivation.
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:
```scala
@derive(encoder)
Expand All @@ -20,15 +27,16 @@ case class Foo[@phantom A, B](...)
implicit def encoder$macro$1[A, B: Encoder]: Encoder.AsObject[Foo[A, B]] = ...
```

`Foo` can be also a newtype in form of [estatico](https://github.com/estatico/scala-newtype) or [supertagged](https://github.com/rudogma/scala-supertagged) libraries.
`Foo` can also be a newtype in form of [estatico](https://github.com/estatico/scala-newtype) or [supertagged](https://github.com/rudogma/scala-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 val`s should be inserted.

If you have problems with initialization order you can optionally put the
`insertInstancesHere()` to the body of your companion object to specify the place where `implicit val`s should be inserted.
## Making your own derivation.

First, extend the object (companion object for your type would be the best) from one of the [`InstanceDef` traits](https://github.com/tofu-tf/derevo/blob/supertagged/core/src/main/scala/derevo/package.scala#L13:L21)).
## Making your own derivation.
First, extend the object (the companion object for your type is the best option) from one of the [`InstanceDef` traits](https://github.com/tofu-tf/derevo/blob/supertagged/core/src/main/scala/derevo/package.scala#L13:L21).

Then implement method `instance` for your object so it could derive corresponding instance.
Then implement the `instance` method for your object so it could derive the corresponding instance.
Example:
```scala
trait TypeClass[A] {
Expand All @@ -40,10 +48,19 @@ object TypeClass extends Derivaton[TypeClass] {
}
```

Also, you can define additional methods `def apply(...)` or `def foo(...)` in your derivation object. This would allow instance creation as
`@derive(TypeClass(...))` or `@derive(TypeClass.foo(...))`
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

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 should have method `newtype[R]` with a single type parameter, that will receive underlying type, and should return another object which has method `instance` that should work as described earlier.

## Expression table

Expand All @@ -52,28 +69,19 @@ To support `newtype` derivation, extend your object with (`NewtypeDerivation`)[h
| `@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` |

| `@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 corresponding macro function is provided by another library.
`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:
```scala
@delegating("full.qualified.method.path")
```
Then call `macro Derevo.delegate`, `macro Derevo.delegateParams`, `macro Derevo.delegateParams2` or `macro Derevo.delegateParams3` in the corresponding methods.
Example could be found [here](https://github.com/tofu-tf/derevo/blob/supertagged/circe/src/main/scala/derevo/circe/circe.scala).

| CI | Release |
| --- | --- |
| ![Scala CI](https://github.com/manatki/derevo/workflows/Scala%20CI/badge.svg) | [![Maven Central](https://img.shields.io/maven-central/v/org.manatki/derevo-core_2.12.svg)](https://search.maven.org/search?q=derevo) |

## Breaking changes in 0.11
`org.manatki.derevo` pkg was shortened to `derevo`.
An example can be found [here](https://github.com/tofu-tf/derevo/blob/supertagged/circe/src/main/scala/derevo/circe/circe.scala).

Use [scalafix](https://scalacenter.github.io/scalafix/docs/users/installation) and [this rule](https://gist.github.com/REDNBLACK/9bc56ad71e4b01a63001339fa61b4cfd) for migration

## Installation
For Scala 2.12 and older:
Expand All @@ -87,7 +95,7 @@ scalacOptions += "-Ymacro-annotations"
```

## IntelliJ Integration
Provides full support and visibility of implicits declared in `@derive` annotation.
Provides full support and visibility of implicits declared in the `@derive` annotation.

To activate, simply click 'Yes' on the extensions popup, after adding any of the `derevo-` integration libraries to your project.
![](https://i.imgur.com/E6BKTeH.png)
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name := "derevo"
import com.typesafe.sbt.SbtGit.git

val publishVersion = "0.12.0"
val publishVersion = "0.12.1"

val common = List(
scalaVersion := "2.13.4",
Expand Down
13 changes: 7 additions & 6 deletions circe/src/main/scala/derevo/circe/circe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import derevo.{Derevo, Derivation, PolyDerivation, delegating}
import io.circe.derivation.renaming
import io.circe.{Codec, Decoder, Encoder}
import derevo.NewTypeDerivation
import derevo.SpecificDerivation

@delegating("io.circe.derivation.deriveDecoder")
object decoder extends Derivation[Decoder] with NewTypeDerivation[Decoder] {
Expand All @@ -18,7 +19,7 @@ object decoder extends Derivation[Decoder] with NewTypeDerivation[Decoder] {
}

@delegating("io.circe.derivation.deriveEncoder")
object encoder extends PolyDerivation[Encoder, Encoder.AsObject] with NewTypeDerivation[Encoder] {
object encoder extends SpecificDerivation[Encoder, Encoder.AsObject, Encoder] 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 @@ -29,7 +30,7 @@ object encoder extends PolyDerivation[Encoder, Encoder.AsObject] with NewTypeDer
}

@delegating("io.circe.derivation.deriveCodec")
object codec extends PolyDerivation[Codec, Codec.AsObject] with NewTypeDerivation[Codec] {
object codec extends SpecificDerivation[Codec, Codec.AsObject, Codec] 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 All @@ -45,12 +46,12 @@ object snakeDecoder extends Derivation[Decoder] {
}

@delegating("io.circe.derivation.deriveEncoder", renaming.snakeCase, None)
object snakeEncoder extends PolyDerivation[Encoder, Encoder.AsObject] {
object snakeEncoder extends SpecificDerivation[Encoder, Encoder.AsObject, Encoder] {
def instance[A]: Encoder.AsObject[A] = macro Derevo.delegate[Encoder.AsObject, A]
}

@delegating("io.circe.derivation.deriveCodec", renaming.snakeCase, false, None)
object snakeCodec extends PolyDerivation[Codec, Codec.AsObject] {
object snakeCodec extends SpecificDerivation[Codec, Codec.AsObject, Encoder] {
def instance[A]: Codec.AsObject[A] = macro Derevo.delegate[Codec.AsObject, A]
}

Expand All @@ -60,11 +61,11 @@ object kebabDecoder extends Derivation[Decoder] {
}

@delegating("io.circe.derivation.deriveEncoder", renaming.kebabCase, None)
object kebabEncoder extends PolyDerivation[Encoder, Encoder.AsObject] {
object kebabEncoder extends SpecificDerivation[Encoder, Encoder.AsObject, Encoder] {
def instance[A]: Encoder.AsObject[A] = macro Derevo.delegate[Encoder.AsObject, A]
}

@delegating("io.circe.derivation.deriveCodec", renaming.kebabCase, false, None)
object kebabCodec extends PolyDerivation[Codec, Codec.AsObject] {
object kebabCodec extends SpecificDerivation[Codec, Codec.AsObject, Encoder] {
def instance[A]: Codec.AsObject[A] = macro Derevo.delegate[Codec.AsObject, A]
}
20 changes: 20 additions & 0 deletions circe/src/test/scala/derevo/circe/CirceDerivationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import io.circe.parser._
import io.circe.derivation.renaming
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers
import io.estatico.newtype.macros.newtype
import CirceDerivationSpec.{Moe, Nya}

class CirceDerivationSpec extends AnyFlatSpec with Matchers {
"Circe derivation" should "derive simple codecs" in {
Expand Down Expand Up @@ -87,6 +89,15 @@ class CirceDerivationSpec extends AnyFlatSpec with Matchers {
it should "reject decode with inserted params" in {
decode[ShelloBaz](shelloKebab).left.getOrElse(null) mustBe a[DecodingFailure]
}

it should "encode object newtype" in {
val bar: SealedTrait = SealedTrait.Bar(143)
assert(Moe(bar).asJson === (bar).asJson)
}

it should "encode string newtype" in {
assert(Nya("uwu").asJson === "uwu".asJson)
}
}

@derive(encoder(identity, Some("type")), decoder(identity, false, Some("type")))
Expand All @@ -98,5 +109,14 @@ object SealedTrait {

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

object CirceDerivationSpec {
@derive(encoder, decoder)
@newtype
case class Moe(tutu: SealedTrait)

@derive(encoder, decoder)
@newtype
case class Nya(string: String)
}
26 changes: 14 additions & 12 deletions core/src/main/scala/derevo/Derevo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Derevo(val c: blackbox.Context) {

val instanceDefs = Vector(
)
val IsSpecificDerivation = isInstanceDef[PolyDerivation[Any, Any]]()
val IsSpecificDerivation = isInstanceDef[SpecificDerivation[Any, Any, Any]]()
val IsDerivation = isInstanceDef[Derivation[Any]]()

val HKDerivation = new DerivationList(
Expand Down Expand Up @@ -188,7 +188,8 @@ class Derevo(val c: blackbox.Context) {
}

if (allTparams.isEmpty) {
val resT = mkAppliedType(mode.to, tq"$typRef")
val resTc = if (newType.isDefined) mode.newtype else mode.to
val resT = mkAppliedType(resTc, tq"$typRef")
q"""
@java.lang.SuppressWarnings(scala.Array("org.wartremover.warts.All", "scalafix:All", "all"))
implicit val $tn: $resT = $call
Expand Down Expand Up @@ -244,6 +245,7 @@ class Derevo(val c: blackbox.Context) {
val name: String,
val from: Type,
val to: Type,
val newtype: Type,
val drop: Int,
val cascade: Boolean
)
Expand All @@ -253,25 +255,25 @@ class Derevo(val c: blackbox.Context) {
val name = c.freshName(mangledName)

c.typecheck(obj).tpe match {
case IsSpecificDerivation(f, t, d) => new NameAndTypes(name, f, t, d, true)
case IsDerivation(f, t, d) => new NameAndTypes(name, f, t, d, true)
case HKDerivation(f, t, d) => new NameAndTypes(name, f, t, d, false)
case _ => abort(s"$obj seems not extending InstanceDef traits")
case IsSpecificDerivation(f, t, nt, d) => new NameAndTypes(name, f, t, nt, d, true)
case IsDerivation(f, t, nt, d) => new NameAndTypes(name, f, t, nt, d, true)
case HKDerivation(f, t, nt, d) => new NameAndTypes(name, f, t, nt, d, false)
case _ => abort(s"$obj seems not extending InstanceDef traits")
}
}

class DerivationList(ds: IsInstanceDef*) {
def unapply(objType: Type): Option[(Type, Type, Int)] =
def unapply(objType: Type): Option[(Type, Type, Type, Int)] =
ds.iterator.flatMap(_.unapply(objType)).collectFirst { case x => x }
}

class IsInstanceDef(t: Type, drop: Int) {
val constrSymbol = t.typeConstructor.typeSymbol
def unapply(objType: Type): Option[(Type, Type, Int)] =
val constrSymbol = t.typeConstructor.typeSymbol
def unapply(objType: Type): Option[(Type, Type, Type, Int)] =
objType.baseType(constrSymbol) match {
case TypeRef(_, _, List(tc)) => Some((tc, tc, drop))
case TypeRef(_, _, List(from, to)) => Some((from, to, drop))
case _ => None
case TypeRef(_, _, List(tc)) => Some((tc, tc, tc, drop))
case TypeRef(_, _, List(from, to, nt)) => Some((from, to, nt, drop))
case _ => None
}
}
def isInstanceDef[T: TypeTag](dropTParams: Int = 0) = new IsInstanceDef(typeOf[T], dropTParams)
Expand Down
19 changes: 10 additions & 9 deletions core/src/main/scala/derevo/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ package derevo {

//* numeration according to https://docs.tofu.tf/docs/internal/kind-enumeration
sealed trait InstanceDef
trait Derivation[TC[_]] extends InstanceDef
trait DerivationKN1[TC[f[_]]] extends InstanceDef
trait DerivationKN2[TC[bf[_, _]]] extends InstanceDef
trait DerivationKN3[TC[alg[f[_]]]] extends InstanceDef
trait DerivationKN4[TC[tr[f[_], _]]] extends InstanceDef
trait DerivationKN5[TC[tf[_, _, _]]] extends InstanceDef
trait DerivationKN11[TC[alg[bf[_, _]]]] extends InstanceDef
trait DerivationKN17[TC[alg[btr[_, _], _, _]]] extends InstanceDef
trait PolyDerivation[FromTC[_], ToTC[_]] extends InstanceDef
trait Derivation[TC[_]] extends InstanceDef
trait DerivationKN1[TC[f[_]]] extends InstanceDef
trait DerivationKN2[TC[bf[_, _]]] extends InstanceDef
trait DerivationKN3[TC[alg[f[_]]]] extends InstanceDef
trait DerivationKN4[TC[tr[f[_], _]]] extends InstanceDef
trait DerivationKN5[TC[tf[_, _, _]]] extends InstanceDef
trait DerivationKN11[TC[alg[bf[_, _]]]] extends InstanceDef
trait DerivationKN17[TC[alg[btr[_, _], _, _]]] extends InstanceDef
trait SpecificDerivation[FromTC[_], ToTC[_], NT[_]] extends InstanceDef

}

Expand All @@ -34,4 +34,5 @@ package object derevo {
type DerivationBi2[TC[alf[bf[_, _]]]] = DerivationKN11[TC]
type DerivationTr[TC[T[f[_], a]]] = DerivationKN4[TC]
type DerivationBiTr[TC[T[f[_, _], a, b]]] = DerivationKN17[TC]
type PolyDerivation[FromTC[_], ToTC[_]] = SpecificDerivation[FromTC, ToTC, ToTC]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ package object reactivemongo {

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

@delegating("reactivemongo.bson.Macros.reader")
object bsonDocumentReader
extends PolyDerivation[BsonValueReader, BSONDocumentReader] with NewTypeDerivation[BSONDocumentReader] {
extends SpecificDerivation[BsonValueReader, BSONDocumentReader, BsonValueReader]
with NewTypeDerivation[BSONDocumentReader] {
def instance[A]: BSONDocumentReader[A] = macro Derevo.delegate[BSONDocumentReader, A]
}
}
6 changes: 4 additions & 2 deletions tethys/src/main/scala/derevo/tethys/tethys.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package derevo.tethys

import derevo.{Derevo, Derivation, PolyDerivation, delegating}
import derevo.{Derevo, Derivation, delegating}
import tethys.derivation.builder._
import tethys.{JsonObjectWriter, JsonReader, JsonWriter}
import derevo.NewTypeDerivation
import derevo.SpecificDerivation

@delegating("tethys.derivation.semiauto.jsonReader")
object tethysReader extends Derivation[JsonReader] with NewTypeDerivation[JsonReader] {
Expand All @@ -14,7 +15,8 @@ object tethysReader extends Derivation[JsonReader] with NewTypeDerivation[JsonRe
}

@delegating("tethys.derivation.semiauto.jsonWriter")
object tethysWriter extends PolyDerivation[JsonWriter, JsonObjectWriter] with NewTypeDerivation[JsonWriter] {
object tethysWriter
extends SpecificDerivation[JsonWriter, JsonObjectWriter, JsonWriter] with NewTypeDerivation[JsonWriter] {
def instance[A]: JsonObjectWriter[A] = macro Derevo.delegate[JsonObjectWriter, A]

def apply[A](arg: WriterDerivationConfig): JsonObjectWriter[A] =
Expand Down