playframework icon indicating copy to clipboard operation
playframework copied to clipboard

Scala 3 opaque types are not supported as path parameters

Open colin-lamed opened this issue 1 year ago • 4 comments

Play Version

3.0.3

API

Scala

Expected Behavior

For an opaque type with PathBindable, e.g.

opaque type TeamName = String

implicit def teamNamePathBindable(using strBinder: PathBindable[String]): PathBindable[TeamName] =
    strBinder.transform(TeamName.apply, _.toString())

and a Controller

@Singleton
class TeamsController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
  def team(teamName: TeamName): Action[AnyContent] =
    Action { OK("Hello " + teamName) }
}

When creating a route with the path bindable, e.g.

GET        /teams/:teamName  TeamsController.team(teamName: TeamName)

It should generate a Routes file for this endpoint

Actual Behavior

The compiler throws

[error] /conf/app.routes:5:1: model.TeamName is not a class type
[error] GET        /teams/:teamName    TeamsController.team(teamName: TeamName)
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

Note, this can be implemented when providing the generated routes manually when the HandlerDef. parameterTypes are suppressed:

override def routes: Router.Routes = {
    case routeMatch(params@_) =>
      call(params.fromPath[TeamName]("teamName", None)) { (teamName) =>
        createInvoker(
          fakeCall   = teamsController.team(fakeValue[TeamName]),
          handlerDef = HandlerDef(
            classLoader    = this.getClass.getClassLoader,
            routerPackage  = "app",
            controller     = "TeamsController",
            method         = "team",
            parameterTypes = Seq.empty,//Seq(classOf[TeamName]), // This is where opaque types don't work
            verb           = "GET",
            path           = this.prefix + """teams/""" + "$" + """teamName<[^/]+>/team""",
            comments       = "",
            modifiers      = Seq.empty
          )
        ).call(teamsController.team(teamName))
      }
  }

  private lazy val routeMatch = Route("GET",
    PathPattern(List(StaticPart(this.prefix), StaticPart(this.defaultPrefix), StaticPart("teams/"), DynamicPart("teamName", """[^/]+""", encodeable=true), StaticPart("/team")))
  )

colin-lamed avatar Jun 19 '24 12:06 colin-lamed

hmm.... I will not work on this right now, not sure yet if/what we can do on Play's side here. If you come up with a PR of course I am happy to review it.

mkurz avatar Jun 25 '24 10:06 mkurz

Looking at https://github.com/playframework/playframework/blob/03149e7f022e12b70eda882a85c9ca5768748b2a/dev-mode/play-routes-compiler/src/main/twirl/play/routes/compiler/inject/forwardsRouter.scala.twirl#L68 the problem is with classOf[SomeOpaqueType]. I've found a possible fix in the routes file but not sure where else opaque types may fail. Hence this isn't a PR.

@call.parameters.filterNot(_.isEmpty).map(params => params.map("classOf[" + _.typeNameReal + "]").mkString(", ")).map("Seq(" + _ + ")").getOrElse("Nil"), becomes

@call.parameters.filterNot(_.isEmpty).map(params => params.map("scala.reflect.classTag[" + _.typeNameReal + "].runtimeClass").mkString(", ")).map("Seq(" + _ + ")").getOrElse("Nil"),

This translates the opaque type to it's underlying class rather than trying to get the class directly and seems to work for cases that broke with it as it was.

AndySpaven avatar Sep 10 '24 09:09 AndySpaven