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

Circe's KeyDecoder and KeyEncoder derivations? #239

Closed
gvolpe opened this issue Mar 18, 2021 · 10 comments
Closed

Circe's KeyDecoder and KeyEncoder derivations? #239

gvolpe opened this issue Mar 18, 2021 · 10 comments

Comments

@gvolpe
Copy link
Contributor

gvolpe commented Mar 18, 2021

AFAIU the derivations supported by derevo are the ones supported by circe-derivation, is that right? If so, then I guess it is not possible to derive KeyDecoder and KeyEncoder instances without upstream support?

Anyhow, my only need for now is to support derivation for these typeclasses for newtypes. Here's a simple example:

@derive(decoder, encoder, eqv, show, uuid)
@newtype
case class ItemId(value: UUID)
object ItemId {
  implicit val keyEncoder: KeyEncoder[ItemId] = deriving
  implicit val keyDecoder: KeyDecoder[ItemId] = deriving
}

There are KeyDecoder[UUID] and KeyEncoder[UUID] instances so this works but I wanted to go a bit further and get Derevo to do this for me. This is what I come up with.

import derevo.{ Derivation, NewTypeDerivation }
import io.circe.KeyDecoder
import magnolia.{ CaseClass, Magnolia }

object keyDecoder extends Derivation[KeyDecoder] with NewTypeDerivation[KeyDecoder] {
  type Typeclass[T] = KeyDecoder[T]

  def combine[T](ctx: CaseClass[KeyDecoder, T]): KeyDecoder[T] = new KeyDecoder[T] {
    def apply(key: String): Option[T] =
      ctx.parameters.toList match {
        case (p :: _) => p.typeclass.apply(key).map(_.asInstanceOf[T])
        case _        => None
      }
  }

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

Do you see anything wrong?

I did something similiar for KeyEncoder but this one is weird because I need to return a String and not sure what the empty case should be.

object keyEncoder extends Derivation[KeyEncoder] with NewTypeDerivation[KeyEncoder] {
  type Typeclass[T] = KeyEncoder[T]

  def combine[T](ctx: CaseClass[KeyEncoder, T]): KeyEncoder[T] = new KeyEncoder[T] {
    def apply(key: T): String =
      ctx.parameters.toList match {
        case (p :: _) => p.typeclass.apply(key.asInstanceOf[p.PType])
        case _        => "error"
      }
  }

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

This works but I wonder if there is any downside or something wrong in my implementations?

@derive(decoder, encoder, keyDecoder, keyEncoder, eqv, show, uuid)
@newtype
case class ItemId(value: UUID)

Appreciate your help, I've never used Magnolia before.

@Odomontois
Copy link
Member

Odomontois commented Apr 7, 2021

@gvolpe

I don't think .asInstanceOf[p.Type] is a good idea here
We could think of several possibilities

1. Derive instances only for newtypes.

Here you don't need magnolia at all. You can demand newtype-only derivation

object keyEncoder extends Derivation[KeyEncoder] with NewTypeDerivation[KeyEncoder]{
    def instance(implicit x: OnlyNewtypes): Nothing = x.absurd
}

object keyDecoder extends Derivation[KeyDecoder] with NewTypeDerivation[KeyDecoder]{
    def instance(implicit x: OnlyNewtypes): Nothing = x.absurd
}
    
@implicitNotFound("use keyEncoder and keyDecoder annotations only for newtypes")
abstract final class OnlyNewtypes{
   def absurd = Nothing
}

2. Derive instances for newtypes any single-parameter case classes

Actually I don't know exactly how to compile-time check for single-variabilty, I suppose it may involve shapeless or custom macro
But you have to use ctx.construct to create such instances

3. Derive instances for newtypes and case classes of any arity

I suppose we can define some general scheme, such as using some separators in the keys to derive variable

class keyDecoder(sep: String = "::") {
  type Typeclass[T] = KeyDecoder[T]

  def combine[T](ctx: CaseClass[KeyDecoder, T]): KeyDecoder[T] =
    if (ctx.isObject) key => if (key == ctx.typeName.short) Some(ctx.rawConstruct(Seq.empty)) else None
    else { key =>
      val parts = key.split(sep)
      if (parts.length != ctx.parameters.length) None
      else ctx.constructMonadic(p => p.typeclass.apply(parts(p.index)))
    }

  def dispatch[T](ctx: SealedTrait[KeyDecoder, T]): KeyDecoder[T] =
    key => ctx.subtypes.view.flatMap(_.typeclass(key)).headOption

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

object keyDecoder extends keyDecoder("::") with Derivation[KeyDecoder] with NewTypeDerivation[KeyDecoder]

class keyEncoder(sep: String = "::") {
  type Typeclass[T] = KeyEncoder[T]

  def combine[T](ctx: CaseClass[KeyEncoder, T]): KeyEncoder[T] =
    if (ctx.isObject) obj => ctx.typeName.short
    else { cc =>
      ctx.parameters.view.map(p => p.typeclass(p.dereference(cc))).mkString("::")
    }

  def dispatch[T](ctx: SealedTrait[KeyEncoder, T]): KeyEncoder[T] =
    obj => ctx.dispatch(obj)(sub => sub.typeclass(sub.cast(obj)))

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

object keyEncoder extends keyEncoder("::") with Derivation[KeyEncoder] with NewTypeDerivation[KeyEncoder]

This will handle most of basic cases with default separator as well as simple type for creating key encoders with custom separators

@gvolpe
Copy link
Contributor Author

gvolpe commented Apr 7, 2021

@Odomontois thanks a lot! Option 3 seems great, got it working but I have no idea how to make the keyEncoder one simpler, any pointers?

Also, I have another one for Http4s' Query Params, and it's probably completely wrong even if it works 😄 If you have any spare time, I would appreciate if you could have a look: https://github.com/gvolpe/pfps-shopping-cart/blob/second-edition/modules/core/src/main/scala/shop/ext/http4s/queryParam.scala

I added a new Typeclass Derivation chapter to the book, which is centered around Derevo, would you be interested in giving it a proof-read once I have something presentable? It would mean a lot 😇

@gvolpe
Copy link
Contributor Author

gvolpe commented Apr 7, 2021

Oh you just updated the comment, nice! 😃

@Odomontois
Copy link
Member

@gvolpe Sorry, unlike me you react so fast, I've updated my comment with more comprehensive solution

@Odomontois
Copy link
Member

@gvolpe Yeah I would gladly pre-read your book!

@gvolpe
Copy link
Contributor Author

gvolpe commented Apr 7, 2021

That's great to hear, got an email to contact you? If you don't want to make it public, you can ping me first at hello at gvolpe dot com.

@Odomontois
Copy link
Member

@gvolpe odomontois@gmail.com

@gvolpe
Copy link
Contributor Author

gvolpe commented Apr 13, 2021

@Odomontois sorry to bother again, I was thinking, do you think these instances could be part of Derevo? It seems something very common to have. Happy to submit a PR with these implementations.

@Odomontois
Copy link
Member

@gvolpe that would be great

@gvolpe
Copy link
Contributor Author

gvolpe commented Apr 13, 2021

Here we go :) #257

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants