Ошибка компиляции: экземпляр не найден: преобразование [models.Errorcode, _ <: Product] в Scala 3 Playframework

Недавно я переносил проект 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-json. Возможно, вы захотите поднять это как проблему/обсуждение на их GitHub.

Gaël J 02.07.2024 22:15
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
71
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Этот код компилируется в 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.

Другие вопросы по теме