Skip to content

MeteoAPI HW #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.2.3"
kotlin("plugin.serialization") version "1.5.31"
}

group = "hu.kotlin.feladat.ms"
Expand All @@ -15,8 +16,20 @@ dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.ktor:ktor-client-okhttp:1.6.6")
implementation("io.ktor:ktor-client-core:1.6.6")
implementation("io.ktor:ktor-client-json:1.6.6")
implementation("io.ktor:ktor-client-serialization:1.6.6")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.1.3") // Spring Cloud OpenFeign integration
implementation("io.github.openfeign:feign-okhttp:11.2")
implementation("io.github.openfeign:feign-jackson:11.2")
implementation("org.reactivestreams:reactive-streams:1.0.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.6.0")
testImplementation("org.springframework.boot:spring-boot-starter-test:3.2.3")
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.4.1")
testImplementation("com.github.tomakehurst:wiremock-standalone:3.0.1")
}

tasks.test {
Expand All @@ -25,4 +38,4 @@ tasks.test {

kotlin {
jvmToolchain(17)
}
}
24 changes: 24 additions & 0 deletions app/src/main/kotlin/configuration/FeignConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hu.vanio.kotlin.feladat.ms.configuration

import feign.Feign
import feign.jackson.JacksonDecoder
import feign.jackson.JacksonEncoder
import feign.okhttp.OkHttpClient
import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FeignConfig {
@Value("\${base.url.weather.api}")
private lateinit var weatherApiBaseUrl: String
@Bean
fun weatherFeignClient(): WeatherFeignClient {
return Feign.builder()
.client(OkHttpClient())
.encoder(JacksonEncoder())
.decoder(JacksonDecoder())
.target(WeatherFeignClient::class.java, weatherApiBaseUrl)
}
}
33 changes: 33 additions & 0 deletions app/src/main/kotlin/controller/WeatherController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package hu.vanio.kotlin.feladat.ms.controller

import hu.vanio.kotlin.feladat.ms.data.DailyAverageData
import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer
import hu.vanio.kotlin.feladat.ms.service.WeatherService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class WeatherController(private val weatherService: WeatherService) {
@GetMapping("/weather")
suspend fun getWeeklyTempData(): ResponseEntity<DailyTempDataContainer> {
return ResponseEntity.ok(weatherService.getDailyTempData())
}

@GetMapping("/average")
suspend fun getAverageTemps(): ResponseEntity<List<DailyAverageData>> {
return ResponseEntity.ok(weatherService.getDailyAverageTemp())
}

@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgumentException(e: IllegalArgumentException): ResponseEntity<*> {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid request: ${e.message}")
}

@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<*> {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred: ${e.message}")
}
}
5 changes: 5 additions & 0 deletions app/src/main/kotlin/data/DailyAverageData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package hu.vanio.kotlin.feladat.ms.data

import java.time.LocalDate

data class DailyAverageData(var date: LocalDate, var average_temp: Double)
6 changes: 6 additions & 0 deletions app/src/main/kotlin/data/DailyTempData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hu.vanio.kotlin.feladat.ms.data

import java.time.LocalDate
import java.time.LocalTime

data class DailyTempData(var date: LocalDate, val hourlyTempData: Map<LocalTime, Double>)
5 changes: 5 additions & 0 deletions app/src/main/kotlin/data/DailyTempDataContainer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package hu.vanio.kotlin.feladat.ms.data

import java.time.LocalDate

data class DailyTempDataContainer(var from: LocalDate?, var to: LocalDate?, val dailyTempData: List<DailyTempData>)
12 changes: 12 additions & 0 deletions app/src/main/kotlin/data/HourlyData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hu.vanio.kotlin.feladat.ms.data

import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.Serializable

@Serializable
data class HourlyData(
@JsonProperty("time")
val time: List<String>?,
@JsonProperty("temperature_2m")
val temperature_2m: List<Double>?
)
11 changes: 11 additions & 0 deletions app/src/main/kotlin/data/HourlyUnits.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hu.vanio.kotlin.feladat.ms.data

import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.Serializable

@Serializable
data class HourlyUnits(
@JsonProperty("time") val time: String,
@JsonProperty("temperature_2m") val temperature_2m: String
) {
}
26 changes: 26 additions & 0 deletions app/src/main/kotlin/data/WeatherResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package hu.vanio.kotlin.feladat.ms.data

import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.Serializable

@Serializable
data class WeatherResponse(
@JsonProperty("latitude")
val latitude: Double,
@JsonProperty("longitude")
val longitude: Double,
@JsonProperty("generationtime_ms")
val generationtime_ms: Double,
@JsonProperty("utc_offset_seconds")
val utc_offset_seconds: Int,
@JsonProperty("timezone")
val timezone: String,
@JsonProperty("timezone_abbreviation")
val timezone_abbreviation: String,
@JsonProperty("elevation")
val elevation: Double,
@JsonProperty("hourly_units")
val hourly_units: HourlyUnits,
@JsonProperty("hourly")
val hourly: HourlyData
)
11 changes: 11 additions & 0 deletions app/src/main/kotlin/httpclient/WeatherFeignClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hu.vanio.kotlin.feladat.ms.httpclient

import feign.RequestLine
import hu.vanio.kotlin.feladat.ms.data.WeatherResponse
import org.springframework.cloud.openfeign.FeignClient

@FeignClient(name = "weatherFeignClient")
interface WeatherFeignClient {
@RequestLine("GET /forecast?latitude=47.4984&longitude=19.0404&hourly=temperature_2m&timezone=auto")
fun getWeather(): WeatherResponse
}
68 changes: 68 additions & 0 deletions app/src/main/kotlin/parser/WeatherResponseParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package hu.vanio.kotlin.feladat.ms.parser

import hu.vanio.kotlin.feladat.ms.data.DailyTempData
import hu.vanio.kotlin.feladat.ms.data.HourlyData
import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter

@Service
class WeatherResponseParser {

@Throws(IllegalArgumentException::class)
fun groupToDailyData(hourlyData: HourlyData): DailyTempDataContainer {
val (time, temp) = validateData(hourlyData)
val dailyTempData = groupByDays(time, temp)

return dailyTempDataContainer(dailyTempData)
}

private fun groupByDays(time: List<String>, temp: List<Double>): List<DailyTempData> {
val dailyTempDataList = mutableListOf<DailyTempData>()
val days = time.map { LocalDate.parse(it.substring(0, 10)) }.distinct()
for (day in days) {
val hourlyTempsForDay = time.zip(temp)
.filter { it.first.toDate() == day }
.map { it.first.toTime() to it.second }
.toMap()

dailyTempDataList.add(DailyTempData(day, hourlyTempsForDay))
}
return dailyTempDataList
}

private fun validateData(hourlyData: HourlyData): Pair<List<String>, List<Double>> {
val time = hourlyData.time
val temp = hourlyData.temperature_2m

if (time.isNullOrEmpty()) {
throw IllegalArgumentException("No time data available")
}

if (temp.isNullOrEmpty()) {
throw IllegalArgumentException("No temp data available")
}

return time to temp
}

private fun dailyTempDataContainer(dailyTempDataList: List<DailyTempData>): DailyTempDataContainer {
val from = dailyTempDataList.first().date
val to = dailyTempDataList.last().date
return DailyTempDataContainer(from, to, dailyTempDataList)
}

private fun String.toDate(): LocalDate {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
return LocalDate.parse(this, formatter)
}

private fun String.toTime(): LocalTime {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
val dateTime = LocalDateTime.parse(this, formatter)
return dateTime.toLocalTime()
}
}
18 changes: 18 additions & 0 deletions app/src/main/kotlin/service/TempCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hu.vanio.kotlin.feladat.ms.service

import hu.vanio.kotlin.feladat.ms.data.DailyTempData
import org.springframework.stereotype.Service

@Service
class TempCalculator {
fun getAverageDailyTemp(dailyTempData: DailyTempData): Double {
if (dailyTempData.hourlyTempData.isEmpty()){
throw IllegalArgumentException("Hourly temperature data for ${dailyTempData.date} is empty.")
}

return dailyTempData.hourlyTempData.values.average()
}


}

35 changes: 35 additions & 0 deletions app/src/main/kotlin/service/WeatherService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package hu.vanio.kotlin.feladat.ms.service

import hu.vanio.kotlin.feladat.ms.data.DailyAverageData
import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer
import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient
import hu.vanio.kotlin.feladat.ms.parser.WeatherResponseParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Service

@Service
class WeatherService(
private val weatherFeignClient: WeatherFeignClient,
private val weatherResponseParser: WeatherResponseParser,
private val tempCalculator: TempCalculator
) {
@Throws(IllegalArgumentException::class)
suspend fun getDailyTempData(): DailyTempDataContainer {
val response = withContext(Dispatchers.IO) {
weatherFeignClient.getWeather()
}
return weatherResponseParser.groupToDailyData(response.hourly)
}

@Throws(IllegalStateException::class)
suspend fun getDailyAverageTemp(): List<DailyAverageData> {
val dailyAverageTempList = mutableListOf<DailyAverageData>()
val weeklyTempData = getDailyTempData()
for (dailyTempData in weeklyTempData.dailyTempData) {
val average = tempCalculator.getAverageDailyTemp(dailyTempData);
dailyAverageTempList.add(DailyAverageData(dailyTempData.date, average))
}
return dailyAverageTempList
}
}
6 changes: 6 additions & 0 deletions app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
logging.level.org.springframework.context.annotation = DEBUG

server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern='%h %l %u %t "%r" %s %b'

base.url.weather.api=https://api.open-meteo.com/v1
11 changes: 0 additions & 11 deletions app/src/test/kotlin/WeatherAppTest.kt

This file was deleted.

54 changes: 54 additions & 0 deletions app/src/test/kotlin/it/WeatherFeignClientIT.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hu.vanio.kotlin.feladat.ms.it

import com.github.tomakehurst.wiremock.WireMockServer
import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient
import hu.vanio.kotlin.feladat.ms.mock.WeatherAPIMock.Companion.setupMockWeatherAPIResponse
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.junit.jupiter.SpringExtension
import kotlin.test.assertEquals

@SpringBootTest
@ActiveProfiles("test")
@EnableFeignClients
@EnableConfigurationProperties
@ExtendWith(SpringExtension::class)
class WeatherFeignClientIT {
companion object {
private lateinit var wireMockServer: WireMockServer

@BeforeAll
@JvmStatic
fun setUp() {
wireMockServer = WireMockServer(1040)
wireMockServer.start()
setupMockWeatherAPIResponse(wireMockServer)
}

@AfterAll
@JvmStatic
fun tearDown() {
wireMockServer.stop()
}
}

@Autowired
private lateinit var weatherFeignClient: WeatherFeignClient

@Test
fun `test weather feign client get weather`() {
val weatherResponse = weatherFeignClient.getWeather()

assertEquals(47.5, weatherResponse.latitude)
assertEquals(19.0625, weatherResponse.longitude)
assertEquals(168, weatherResponse.hourly.time?.size)
assertEquals(168, weatherResponse.hourly.temperature_2m?.size)
}
}
Loading