Ошибки компиляции TypeLevel Scala для HTTP-сервиса при использовании зависимости cats

Вопрос или проблема

Я пытаюсь написать пример HTTP-сервиса с использованием TypeLevel Scala, который обращается к API Национальной службы погоды. У меня возникают ошибки несоответствия типов и импорт EntityEncoder из пакета cats.effect.IO.

build.sbt:

lazy val http4sVersion    = "1.0.0-M37"
lazy val circeVersion     = "0.14.5"
lazy val sttpVersion      = "3.9.0"
lazy val munitVersion     = "0.7.29"
lazy val logbackVersion   = "1.4.7"

ThisBuild / scalaVersion := "2.13.12"

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-blaze-server" % http4sVersion,
  "org.http4s" %% "http4s-circe"        % http4sVersion,
  "org.http4s" %% "http4s-dsl"          % http4sVersion,
  "io.circe"   %% "circe-generic"       % circeVersion,
  "io.circe"   %% "circe-literal"       % circeVersion,
  "com.softwaremill.sttp.client3" %% "core" % sttpVersion,
  "com.softwaremill.sttp.client3" %% "circe" % sttpVersion,
  "org.typelevel" %% "cats-effect" % "3.5.1",
  "org.scalameta" %% "munit" % munitVersion % Test,
  "ch.qos.logback" % "logback-classic" % logbackVersion
)

WeatherServer.scala:

package weather

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.circe._
import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._

object WeatherServer extends IOApp {

  case class WeatherResponse(forecast: String, temperatureType: String)

  def classifyTemperature(temp: Double): String = {
    if (temp < 10) "холодно"
    else if (temp > 25) "жарко"
    else "умеренно"
  }

  def getWeather(latitude: Double, longitude: Double): IO[WeatherResponse] = {
    val backend = HttpURLConnectionBackend()
    val request = basicRequest
      .get(uri"https://api.weather.gov/points/$latitude,$longitude")
      .response(asJson[Map[String, Any]])

    IO.fromEither(request.send(backend).body)
      .flatMap { body =>
        val forecastUrl = body("properties").asInstanceOf[Map[String, Any]]("forecast").toString
        val forecastRequest = basicRequest.get(uri"$forecastUrl").response(asJson[Map[String, Any]])
        IO.fromEither(forecastRequest.send(backend).body)
          .map { forecastBody =>
            val forecastData = forecastBody("properties").asInstanceOf[Map[String, Any]]("periods").asInstanceOf[List[Map[String, Any]]]
            val todayForecast = forecastData.head("shortForecast").toString
            val temperature = forecastData.head("temperature").toString.toDouble
            WeatherResponse(todayForecast, classifyTemperature(temperature))
          }
      }
  }

  val weatherService = HttpRoutes.of[IO] {
    case GET -> Root / "weather" :? LatitudeQueryParamMatcher(lat) +& LongitudeQueryParamMatcher(lon) =>
      getWeather(lat, lon).flatMap { weather =>
        Ok(weather)
      }
  }

  val httpApp = weatherService.orNotFound

  def run(args: List[String]): IO[ExitCode] =
    BlazeServerBuilder[IO](runtime.compute)
      .bindHttp(8080, "0.0.0.0")
      .withHttpApp(httpApp)
      .serve
      .compile
      .drain
      .as(ExitCode.Success)
}

object LatitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("latitude")
object LongitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("longitude")

Когда я запускаю его с помощью sbt run:

[info] Добро пожаловать в sbt 1.10.2 (Oracle Corporation Java 21)
[info] загрузка определения проекта из /Users/pnwlover/weatherservice/project
[info] загрузка настроек для проекта weatherservice из build.sbt ...
[info] установлен текущий проект weatherservice (в файле build:/Users/pnwlover/weatherservice/)
[info] компиляция 2 исходников Scala в /Users/pnwlover/weatherservice/target/scala-2.13/classes ...
[error] /Users/pnwlover/weatherservice/src/main/scala/weather/WeatherServer.scala:26:12: несоответствие типов;
[error]  найден   : StringContext
[error]  требуется: ?{def uri(x$1: ? >: Double, x$2: ? >: Double): ?}
[error] Обратите внимание, что неявные преобразования не применимы, поскольку они неоднозначны:
[error]  оба метода http4sLiteralsSyntax в трейте LiteralsSyntax типа (sc: StringContext): org.http4s.syntax.LiteralsOps
[error]  и метод UriContext в трейте UriInterpolator типа (sc: StringContext): sttp.client3.package.UriContext
[error]  это возможные функции преобразования от StringContext к ?{def uri(x$1: ? >: Double, x$2: ? >: Double): ?}
[error]       .get(uri"https://api.weather.gov/points/$latitude,$longitude")
[error]            ^
[error] /Users/pnwlover/weatherservice/src/main/scala/weather/WeatherServer.scala:27:23: не удалось найти неявное значение для параметра доказательства типа io.circe.Decoder[Map[String,Any]]
[error]       .response(asJson[Map[String, Any]])
[error]                       ^
[error] /Users/pnwlover/weatherservice/src/main/scala/weather/WeatherServer.scala:46:11: Невозможно преобразовать из weather.WeatherServer.WeatherResponse в Entity, поскольку экземпляр EntityEncoder[cats.effect.IO, weather.WeatherServer.WeatherResponse] не может быть найден.
[error]         Ok(weather)
[error]           ^
[error] найдено три ошибки
[error] (Compile / compileIncremental) Компиляция не удалась
[error] Общее время: 2 с, завершено 25 сентября 2024 г., 15:00:05

Ответ или решение

Ваша проблема может be связана с несколькими аспектами вашего кода. Давайте разберем каждый из возникших ошибок и решим их.

Ошибка 1: Конфликтирование имплиситных функций для uri

Ошибка:

found : StringContext
required: ?{def uri(x$1: ? >: Double, x$2: ? >: Double): ?}

Эта ошибка возникает из-за конфликта между библиотеками Http4s и Sttp. Обе библиотеки определяют свою собственную функцию для интерполяции URI, и компилятор не может определить, какую именно вы хотите использовать. Чтобы существенно решить проблему, вы можете явно указать, какую функцию использовать, с помощью имплиситного импорта.

Решение:

Вы можете указать, какую библиотеку использовать для URI, например так:

import org.http4s.Uri
import org.http4s.syntax.literals._

Затем измените строку вызова uri следующим образом:

.get(uri"https://api.weather.gov/points/${latitude},${longitude}")

Ошибка 2: Не удается найти Decoder для Map[String, Any]

Ошибка:

could not find implicit value for evidence parameter of type io.circe.Decoder[Map[String,Any]]

Эта ошибка говорит о том, что Circe не знает, как декодировать JSON-объекты в Map[String, Any]. Вам следует использовать более конкретные типы для декодирования.

Решение:

Определите классы для вашего ожидаемого JSON-ответа. Вам нужно будет создать модель данных, которая соответствует структуре JSON, которую вы получаете от API. Пример:

case class Properties(forecast: String)
case class WeatherApiResponse(properties: Properties)

Тогда измените ваш метод getWeather, чтобы использовать WeatherApiResponse вместо Map[String, Any]:

val request = basicRequest
  .get(uri"https://api.weather.gov/points/$latitude,$longitude")
  .response(asJson[WeatherApiResponse])

Ошибка 3: Не удается найти EntityEncoder для WeatherResponse

Ошибка:

Cannot convert from weather.WeatherServer.WeatherResponse to an Entity, because no EntityEncoder[cats.effect.IO, weather.WeatherServer.WeatherResponse] instance could be found.

Эта ошибка заключается в том, что Http4s не знает, как кодировать ваш WeatherResponse в HTTP-ответ. Вам нужно предоставить EntityEncoder для него.

Решение:

Для этого вам нужно импортировать и использовать http4s-circe, который предоставит вам нужный EntityEncoder. Убедитесь, что вы импортируете следующий пакет:

import org.http4s.circe._

Если у вас нет необходимого Encoder, вы можете явно добавить его:

implicit val weatherResponseEncoder: EntityEncoder[IO, WeatherResponse] = jsonEncoderOf[WeatherResponse]

Таким образом, ваш updated WeatherServer.scala будет выглядеть примерно так:

import cats.effect._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.circe._
import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._

object WeatherServer extends IOApp with Http4sDsl[IO] {
  case class WeatherResponse(forecast: String, temperatureType: String)
  case class Properties(forecast: String)
  case class WeatherApiResponse(properties: Properties)

  def classifyTemperature(temp: Double): String = {
    if (temp < 10) "cold"
    else if (temp > 25) "hot"
    else "moderate"
  }

  def getWeather(latitude: Double, longitude: Double): IO[WeatherResponse] = {
    val backend = HttpURLConnectionBackend()
    val request = basicRequest
      .get(uri"https://api.weather.gov/points/$latitude,$longitude")
      .response(asJson[WeatherApiResponse])

    IO.fromEither(request.send(backend).body).flatMap { body =>
      val forecastUrl = body.properties.forecast
      val forecastRequest = basicRequest.get(uri"$forecastUrl").response(asJson[Map[String, Any]])
      IO.fromEither(forecastRequest.send(backend).body).map { forecastBody =>
        val forecastData = forecastBody("properties").asInstanceOf[Map[String, Any]]("periods").asInstanceOf[List[Map[String, Any]]]
        val todayForecast = forecastData.head("shortForecast").toString
        val temperature = forecastData.head("temperature").toString.toDouble
        WeatherResponse(todayForecast, classifyTemperature(temperature))
      }
    }
  }

  implicit val weatherResponseEncoder: EntityEncoder[IO, WeatherResponse] = jsonEncoderOf[WeatherResponse]

  val weatherService = HttpRoutes.of[IO] {
    case GET -> Root / "weather" :? LatitudeQueryParamMatcher(lat) +& LongitudeQueryParamMatcher(lon) =>
      getWeather(lat, lon).flatMap { weather =>
        Ok(weather)
      }
  }

  val httpApp = weatherService.orNotFound

  def run(args: List[String]): IO[ExitCode] =
    BlazeServerBuilder[IO]
      .bindHttp(8080, "0.0.0.0")
      .withHttpApp(httpApp)
      .serve
      .compile
      .drain
      .as(ExitCode.Success)
}

object LatitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("latitude")
object LongitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("longitude")

Заключение

Теперь ваш проект должен работать без ошибок компиляции. Проверьте ваш код, сделайте компиляцию проекта и перезапустите сервер. Если появятся новые ошибки, пожалуйста, поделитесь ими, и мы поможем вам решить их.

Оцените материал
Добавить комментарий

Капча загружается...