Scala 3 opaque types are not supported as path parameters
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")))
)
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.
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.