Недавно я переносил проект playframework со scala2 на scala3 и обнаружил ошибку, которая возникает только в scala 3, но не происходит в scala 2:
play.sbt.PlayExceptions$CompilationException: Compilation error[Instance not found: 'Conversion[models.ErrorCode, _ <: Product]']
at play.sbt.PlayExceptions$CompilationException$.apply(PlayExceptions.scala:28)
at play.sbt.PlayExceptions$CompilationException$.apply(PlayExceptions.scala:28)
at scala.Option.map(Option.scala:230)
at play.sbt.run.PlayReload$.$anonfun$taskFailureHandler$8(PlayReload.scala:119)
at scala.Option.map(Option.scala:230)
at play.sbt.run.PlayReload$.taskFailureHandler(PlayReload.scala:92)
at play.sbt.run.PlayReload$.$anonfun$compile$3(PlayReload.scala:144)
at scala.util.Either$LeftProjection.map(Either.scala:573)
at play.sbt.run.PlayReload$.compile(PlayReload.scala:144)
at sbt.PlayRun$.$anonfun$playRunTask$5(PlayRun.scala:99)
Строки, вызывающие ошибку:
implicit val formatError: OFormat[ErrorCode] = Json.format[ErrorCode]
implicit val formatAccountException: OFormat[AccountException] = Json.format[AccountException]
Поскольку это исключение «экземпляр не найден», я предполагаю, что мне нужно использовать новую инструкцию ErrorCode() в каком-то месте кода, но я понятия не имею, где это должно быть, поскольку этот код отлично работает в scala 2, и только эта ошибка происходит в моем проекте Scala 3. Это действительно сбивает с толку
Я просматривал документацию по игровой платформе, но не нашел ничего, даже отдаленно связанного с ошибкой. У меня есть легкое подозрение, что мне нужно создать экземпляр «нового ErrorCode» внутри контроллера, в котором возникает конфликт, а не в классе, но я могу ошибаться. Я оставлю здесь код классов, вызывающих ошибку:
ErrorCode — это просто абстрактный класс для создания типа, который можно расширить. Я попытался сделать его классом случая и использовать классы случая AccountException в качестве функций, которые его возвращали, но это не совсем то поведение, которое мне нужно в моем приложении, мне нужно, чтобы оно возвращало определенные типы.
abstract class ErrorCode {
val code: String = ""
val title: String = ""
val detail: Option[String] = None
}
//case class ErrorCode(code: String, title: String, detail: Option[String])
object ErrorCode {
def apply(codeValue: String, titleValue: String, detailValue: Option[String]): ErrorCode = new ErrorCode {
override val code: String = codeValue
override val title: String = titleValue
override val detail: Option[String] = detailValue
}
def unapply(err: ErrorCode): Option[(String, String, Option[String])] =
Some((err.code, err.title, err.detail))
}
Это AccountException, очень длинный:
abstract class AccountException(val errorCode: ErrorCode) {}
object AccountExceptions {
def apply(errorCode: ErrorCode): AccountException = new AccountException(errorCode) {}
def unapply(accEx: AccountException): Option[ErrorCode] = Some(accEx.errorCode)
case class AccountNotFoundException(resource: String) extends AccountException(AccountNotFound(resource))
case class AppNotFoundException(appKey: String) extends AccountException(AppNotFound(appKey))
case class AppTackConfigurationException(resource: String) extends AccountException(AppTackConfigurationError(resource))
case class InvalidCredentialsException(username: String) extends AccountException(InvalidCredentials(username))
case class MalformedAppleTokenException(userResource: String) extends AccountException(MalformedAppleToken(userResource))
case class MalformedGoogleTokenException(userResource: String) extends AccountException(MalformedGoogleToken(userResource))
case class ResourceNotFoundException(resource: String) extends AccountException(ResourceNotFound(resource))
case class UnknownException(username: String) extends AccountException(UnknownError(username))
case class UserAlreadyExistsException(username: String) extends AccountException(UserAlreadyExists(username))
case class UserNotFoundException(username: String) extends AccountException(UserNotFound(username))
}
object AccountErrorCodes {
case class AccountNotFound(resource: String) extends ErrorCode
{
override val code: String = "OAC07"
override val title: String = "Account not found"
override val detail: Option[String] = Some(resource)
}
case class AppNotFound(appKey: String) extends ErrorCode
{
override val code: String = "AME01"
override val title: String = "App not found"
override val detail: Option[String] = Some(appKey)
}
case class AppTackConfigurationError(configErrorDetail: String) extends ErrorCode
{
override val code: String = "AME07"
override val title: String = "Apptack configuration error"
override val detail: Option[String] = Some(configErrorDetail)
}
case class InvalidCredentials(username: String)extends ErrorCode
{
override val code: String = "OAC02"
override val title: String = "Credential invalid"
override val detail: Option[String] = Some(username)
}
case class MalformedAppleToken(userResource: String)extends ErrorCode
{
override val code: String = "OAC06"
override val title: String = "Malformed Apple sso token"
override val detail: Option[String] = Some(userResource)
}
case class MalformedGoogleToken(userResource: String) extends ErrorCode
{
override val code: String = "OAC06"
override val title: String = "Malformed google sso token"
override val detail: Option[String] = Some(userResource)
}
case class ResourceNotFound(resource: String) extends ErrorCode
{
override val code: String = "OAC07"
override val title: String = "Resource not found"
override val detail: Option[String] = Some(resource)
}
case class UnknownError(username: String) extends ErrorCode
{
override val code: String = "OAC05"
override val title: String = "Unexpected error"
override val detail: Option[String] = Some(username)
}
case class UserAlreadyExists(username: String) extends ErrorCode
{
override val code: String = "OAC03"
override val title: String = "User already exists"
override val detail: Option[String] = Some(username)
}
case class UserNotFound(username: String) extends ErrorCode
{
override val code: String = "OAC01"
override val title: String = "User not found"
override val detail: Option[String] = Some(username)
}
}
Я мог бы продолжить изменение ErrorCode с абстрактного класса на подход Case Class и изменить классы случаев AccountErrorCodes для функций, которые возвращают ErrorCode, но я использую эти конкретные типы для управления HTTP-ответами, поэтому я хотел бы сохранить это поведение:
//this function takes place in the controller
private def exceptionToResult(exception: AccountException): Result = {
exception match {
case e: AccountExceptions.AccountNotFoundException => NotFound(Json.toJson(exception))
case e: AccountExceptions.AppTackConfigurationException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.InvalidCredentialsException => Unauthorized(Json.toJson(exception))
case e: AccountExceptions.MalformedAppleTokenException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.MalformedGoogleTokenException => BadRequest(Json.toJson(exception))
case e: AccountExceptions.ResourceNotFoundException => NotFound(Json.toJson(exception))
case e: AccountExceptions.UnknownException => InternalServerError(Json.toJson(exception))
case e: AccountExceptions.UserAlreadyExistsException => Conflict(Json.toJson(exception))
case e: AccountExceptions.UserNotFoundException => NotFound(Json.toJson(exception))
case _ => InternalServerError(Json.toJson(exception))
}
}
Этот код компилируется в scala 3 и play 3.
import play.api.libs.json.{JsValue, Json, OWrites, Writes}
import play.api.mvc.Result
import play.api.mvc.Results.{InternalServerError, NotFound}
case class ErrorCode(code: String, title: String, detail: Option[String])
implicit val errorCodeWrites: OWrites[ErrorCode] = Json.writes[ErrorCode]
implicit val accountNotFoundExceptionWrites: OWrites[AccountException.AccountNotFoundException] =
Json.writes[AccountException.AccountNotFoundException]
implicit val appNotFoundExceptionWrites: OWrites[AccountException.AppNotFoundException] =
Json.writes[AccountException.AppNotFoundException]
//implicit val accountExceptionWrites: OWrites[AccountException] =
// Json.writes[AccountException]
implicit val accountExceptionWrites: Writes[AccountException] = new Writes[AccountException]:
def writes(o: AccountException): JsValue =
val w = implicitly[Writes[Option[String]]]
val builder = Json.newBuilder
builder += "code" -> o.errorCode.code
builder += "title" -> o.errorCode.title
builder += "detail" -> w.writes(o.errorCode.detail)
builder.result()
enum AccountException(val errorCode: ErrorCode) {
case AccountNotFoundException(resource: String)
extends AccountException(
ErrorCode(code = "OAC07", title = "Account Not Found", Some(resource))
)
case AppNotFoundException(appKey: String)
extends AccountException(
ErrorCode(code = "AME01", title = "App Not Found", Some(appKey))
)
}
def exceptionToResult(exception: AccountException): Result = {
exception match {
case AccountException.AccountNotFoundException(_) =>
NotFound(Json.toJson(exception))
// ....
case _ => InternalServerError(Json.toJson(exception))
}
}
Я не конвертировал весь ваш код и не тестировал его, но надеюсь, что это поможет. Код основан на моем существующем коде из написанного мною приложения.
Я оставляю здесь ответ, поскольку @boggy предположил, что для этого мне нужно реализовать «новую запись».
Это немного по-другому, но это сработало.
implicit val accountExceptionWrites: Writes[AccountException] = (o: AccountException) => {
val w = implicitly[Writes[Option[String]]]
val builder = Json.newBuilder
builder += "code" -> o.errorCode.code
builder += "title" -> o.errorCode.title
builder += "detail" -> w.writes(o.errorCode.detail)
builder.result()
}
//it works with this enum
enum AccountException(val errorCode: ErrorCode) {
case AccountNotFoundException(resource: String) extends AccountException(ErrorCode("OAC07", "Account not found", Some(resource)))
case AppNotFoundException(appKey: String) extends AccountException(ErrorCode("AME01", "App not found", Some(appKey)))
//... and so on and so on
}
К сожалению, ни один из других вариантов не работал в Scala 3, даже неявный Json.writes[AccountException.AccountNotFoundException]
, но все они работали в Scala 2.
Похоже на изменение поведения Scala 3 для play-json. Возможно, вы захотите поднять это как проблему/обсуждение на их GitHub.