Вопрос или проблема
Я пытаюсь написать пример 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")
Заключение
Теперь ваш проект должен работать без ошибок компиляции. Проверьте ваш код, сделайте компиляцию проекта и перезапустите сервер. Если появятся новые ошибки, пожалуйста, поделитесь ими, и мы поможем вам решить их.