http4s / rho

A self documenting DSL built on http4s
Other
295 stars 65 forks source link

Implicit EntityEncoder for circe Encoder breaks compilation #216

Open DStranger opened 6 years ago

DStranger commented 6 years ago

Hi guys,

The following code doesn't compile with a could not find implicit value for parameter hltf: org.http4s.rho.bits.HListToFunc[F,String :: shapeless.HNil,String => F[Test.this.Ok.T[String]]].

import cats.effect.Effect
import io.circe.Encoder
import org.http4s._
import org.http4s.rho.RhoService
import org.http4s.rho.swagger.SwaggerSyntax

trait Auto

class Test[F[_]](swagger: SwaggerSyntax[F])(implicit E: Effect[F]) extends RhoService[F] {
  import swagger._

  private implicit def autoEncoder[A <: Auto: Encoder]: EntityEncoder[F, A] = ???

  "This route allows your to post stuff" **
    POST / "post" ^ EntityDecoder.text[F] |>> { body: String =>
      Ok("you posted " + body)
  }
}

Removing autoEncoder, or removing the io.circe.Encoder constraint fixes the problem.

bryce-anderson commented 6 years ago

Curious, since I believe the interesting part is that there isn't any need for the Auto entity encoder anyway. Do you need to remove the io.circe.Encoder import, or just the constraint in the autoEncoder method?

DStranger commented 6 years ago

@bryce-anderson in this code there's indeed no need for Auto, but I've adapted it from some code that finds this construct useful (adapted from the swagger example). Removing just the constraint works.

DStranger commented 6 years ago

@bryce-anderson the problem resolves if I disable partial unification

bryce-anderson commented 6 years ago

Interesting.

SystemFw commented 6 years ago

that EntityEncoder should not be defined like that. I don't know if it's related to this specific problem or not (probably not), but it might be worth having a look at this regardless: https://github.com/http4s/http4s/issues/1648

DStranger commented 6 years ago

@SystemFw yes, I don't think that's related, the error in http4s/http4s#1648 was caused by jsonEncoderOf requiring an instance of EntityEncoder[F, String], here it's not the case.

SystemFw commented 6 years ago

The puzzling thing here is the interaction with partial unification. I have it seen it happen e.g. wrt order of type parameters, but never sub typing constraints

Igosuki commented 6 years ago

I have the same problem. circe.generic.auto works fine for EntityDecoders but not Encoders with Rho...

Igosuki commented 6 years ago

I fixed it by hard encoding the import statements in the right order : import swaggerSyntax. import io.circe.generic.auto. import org.http4s.circe. import org.http4s.rho.bits.

It's a conflict of implicits between rho.bits, circe and http4s.circe

nightscape commented 6 years ago

@Igosuki could you post a full example? I'm just running into the same problem and I can't seem to figure it out... Or anybody else with a workaround for that matter :wink:

DStranger commented 6 years ago

@nightscape did you happen to find a workaround? )

nightscape commented 6 years ago

Unfortunately not... I converted the problematic code to pure Http4s and left the rest in Rho.

Igosuki commented 6 years ago

Two things to do @nightscape @DStranger

here's the encoders def I'm using :

trait GenericEncoders {
   // Generic
  implicit def jsonEncoder[F[_]: Sync, A <: Product: Encoder]
    : EntityEncoder[F, A] = jsonEncoderOf[F, A]
  implicit def cJsonEncoder[F[_]: Sync]: EntityEncoder[F, Json] =
    jsonEncoderOf[F, Json]
  implicit def valueClassEncoder[A: UnwrappedEncoder]: Encoder[A] = implicitly
}
SystemFw commented 6 years ago

Perhaps someone can open a PR to the rho docs for this?

Igosuki commented 6 years ago

I haven't investigated where the problem comes from exactly since I didn't know the codebase until my last PR... I suspect it has to do with Circe's macros which conflict with rho's Kleisli's and shapeless.

Igosuki commented 6 years ago

Typically, you can get a similar type of error if you use circe.generic.auto and sealed traits with noKnownSubclasses until scala 2.12.4 it's a question of what class gets resolved first when the compiler reaches a certain phase.

chuwy commented 6 years ago

Not sure if it is exactly same, but I see could not find implicit value for parameter hltf compliation error for any endpoint that can return more than one type of HTTP response:

class MinimalService[F[_]: Sync](swaggerSyntax: SwaggerSyntax[F]) extends RhoService[F] {
  import swaggerSyntax._

  "Example route" **
    POST / "endpoint" ^ jsonDecoder[F] |>> { _: Json =>
    if (true) Ok("ok") else Forbidden("forbidden")
  }
}
[error] /Users/chuwy/workspace/server/src/main/scala//server/service/MinimalService.scala:16:40: could not find implicit value for parameter hltf: org.http4s.rho.bits.HListToFunc[F,io.circe.Json :: shapeless.HNil,io.circe.Json => F[_ >: MinimalService.this.Ok.T[String] with MinimalService.this.Forbidden.T[String] <: org.http4s.rho.Result[F,Nothing,Nothing,Nothing,String,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,String,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing]]]
[error]     POST / "endpoint" ^ jsonDecoder[F] |>> { _: Json =>
[error]                                        ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

Works fine either without decoding or with single HTTP status.

UPD: tying MinimalService to IO solved the problem. UPD2: my issue was not related to the one raised here. Changing F[_] to covariant F[+_] solved the problem properly

Igosuki commented 5 years ago

Yeah that's because with IO you can directly pull the concrete IO dsl, which isn't possible with just F : Effect

ChetanBhasin commented 5 years ago

So I'm having the same issue, I think.

The following code works, but if I try to do something like

class ProductRoutes[M[+_]: Effect, F[_]](swaggerSyntax: SwaggerSyntax[M])(F: ProductSyntax[M]) extends RhoRoutes[M] {

  import swaggerSyntax._
  import io.circe.generic.auto._
  import org.http4s.circe.{jsonOf, jsonEncoderOf}

  implicit def productEntityEncoder: EntityEncoder[M, ProductRepresentation] = jsonEncoderOf[M, ProductRepresentation]
  implicit def errorEntityEncoder: EntityEncoder[M, ServiceError] = jsonEncoderOf[M, ServiceError]

  implicit def productEntityDecoder: EntityDecoder[M, ProductRepresentation] = jsonOf[M, ProductRepresentation]
  implicit def errorEntityDecoder: EntityDecoder[M, ServiceError] = jsonOf[M, ServiceError]

  "We don't want to have a real 'root' route anyway..." **
    GET |>> TemporaryRedirect(Uri(path = "/swagger-ui"))

  "This route allows you to post stuff" **
    POST / "post" ^ EntityDecoder.text[M] |>> {
      body: String => Ok("you posted " + body)
    }

  "This route allows you to get the product" **
    GET / "product" / pathVar[String] |>> {
    id: String => F.getProduct(id)
  }

  "Endpoint for creating product" **
    PUT / "product" ^ EntityDecoder[M, ProductRepresentation] |>> {
      product: ProductRepresentation => F.createProduct(product)
    }

}

But it doesn't work if I modify the code to do something like this:

 "This route allows you to get the product" **
    GET / "product" / pathVar[String] |>> {
      id: String =>
        F.getProduct(id) map {
          case Some(prod) => Ok(prod)
          case None => NotFound(ServiceError(s"$id does not exist"))
        }
    }
zarthross commented 5 years ago

@ChetanBhasin your issue is Ok and NotFound return an M[Result[...*]], so when you do F.getProduct.map ... you should really be using flatMap so you aren't doing M[M[Result[...*]].

Also, just random note, class ProductRoutes[M[+_]: Effect, F[_]] the F[_] is unused in your class.