From 8244f2aa60dbab13c222a9b3803b65fe9869415d Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sun, 10 Sep 2023 12:39:00 +0200 Subject: [PATCH] feat: adding validators and error responses. --- .env | 4 +- .version | 2 +- build.gradle.kts | 1 + .../DataNotAvailableYetException.kt | 7 ++- .../exceptions/GlobalExceptionHandler.kt | 27 +++++++++ .../UnprocessableEntityException.kt | 8 +++ .../electricityprices/service/PriceService.kt | 5 +- .../web/controller/PriceController.kt | 60 ++++++++++++++++++- .../web/validaton/ValidDateDay.kt | 36 +++++++++++ 9 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/ie/daithi/electricityprices/exceptions/GlobalExceptionHandler.kt create mode 100644 src/main/kotlin/ie/daithi/electricityprices/exceptions/UnprocessableEntityException.kt create mode 100644 src/main/kotlin/ie/daithi/electricityprices/web/validaton/ValidDateDay.kt diff --git a/.env b/.env index 00f5411..1410937 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ PORT=8080 LOGGING_LEVEL=INFO -MONGODB_URI=mongodb://mongo-node1:27017,mongo-node2:27018,mongo-node3:27019/electicity-prices -SPRING_PROFILES_ACTIVE=price-sync +MONGODB_URI=mongodb://localhost:27017,localhost:27018,localhost:27019/electicity-prices +SPRING_PROFILES_ACTIVE=no-price-sync SYNC_START_DATE=2023-01-01 \ No newline at end of file diff --git a/.version b/.version index 0c9cb69..32bd932 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.11.2 \ No newline at end of file +1.12.0 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2a9b7a9..23b7b95 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-validation") // Swagger implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0") diff --git a/src/main/kotlin/ie/daithi/electricityprices/exceptions/DataNotAvailableYetException.kt b/src/main/kotlin/ie/daithi/electricityprices/exceptions/DataNotAvailableYetException.kt index b511f4f..599a2be 100644 --- a/src/main/kotlin/ie/daithi/electricityprices/exceptions/DataNotAvailableYetException.kt +++ b/src/main/kotlin/ie/daithi/electricityprices/exceptions/DataNotAvailableYetException.kt @@ -1,3 +1,8 @@ package ie.daithi.electricityprices.exceptions -class DataNotAvailableYetException(s: String) : Throwable() \ No newline at end of file +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +class DataNotAvailableYetException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/ie/daithi/electricityprices/exceptions/GlobalExceptionHandler.kt b/src/main/kotlin/ie/daithi/electricityprices/exceptions/GlobalExceptionHandler.kt new file mode 100644 index 0000000..351a7f1 --- /dev/null +++ b/src/main/kotlin/ie/daithi/electricityprices/exceptions/GlobalExceptionHandler.kt @@ -0,0 +1,27 @@ +package ie.daithi.electricityprices.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import jakarta.validation.ConstraintViolationException +import org.springframework.web.context.request.ServletWebRequest +import org.springframework.web.context.request.WebRequest + +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(ConstraintViolationException::class) + fun handleConstraintViolationException( + e: ConstraintViolationException, + webRequest: WebRequest + ): ResponseEntity> { + val errorAttributes = mutableMapOf() + errorAttributes["timestamp"] = System.currentTimeMillis() + errorAttributes["status"] = HttpStatus.UNPROCESSABLE_ENTITY.value() + errorAttributes["error"] = "Unprocessable Entity" + errorAttributes["message"] = e.message ?: "Validation failed" + errorAttributes["path"] = (webRequest as ServletWebRequest).request.requestURI + return ResponseEntity(errorAttributes, HttpStatus.UNPROCESSABLE_ENTITY) + } +} diff --git a/src/main/kotlin/ie/daithi/electricityprices/exceptions/UnprocessableEntityException.kt b/src/main/kotlin/ie/daithi/electricityprices/exceptions/UnprocessableEntityException.kt new file mode 100644 index 0000000..0169c9b --- /dev/null +++ b/src/main/kotlin/ie/daithi/electricityprices/exceptions/UnprocessableEntityException.kt @@ -0,0 +1,8 @@ +package ie.daithi.electricityprices.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + + +@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) +class UnprocessableEntityException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/ie/daithi/electricityprices/service/PriceService.kt b/src/main/kotlin/ie/daithi/electricityprices/service/PriceService.kt index 39c0ee2..79df5ee 100644 --- a/src/main/kotlin/ie/daithi/electricityprices/service/PriceService.kt +++ b/src/main/kotlin/ie/daithi/electricityprices/service/PriceService.kt @@ -39,9 +39,10 @@ class PriceService( return priceRepo.dateTimeBetween(startDate, endDate) } - fun getDailyPriceInfo(dateStr: String?): DailyPriceInfo { - val date = dateStr?.let { LocalDate.parse(it, dateFormatter) } ?: LocalDate.now() + fun getDailyPriceInfo(dateStr: String): DailyPriceInfo? { + val date = LocalDate.parse(dateStr, dateFormatter) val prices = getPrices(start = dateStr, end = dateStr) + if (prices.isEmpty()) return null val cheapestPeriods = getTwoCheapestPeriods(prices, 3) val expensivePeriod = getMostExpensivePeriod(prices, 3) diff --git a/src/main/kotlin/ie/daithi/electricityprices/web/controller/PriceController.kt b/src/main/kotlin/ie/daithi/electricityprices/web/controller/PriceController.kt index b165423..c5eb2af 100644 --- a/src/main/kotlin/ie/daithi/electricityprices/web/controller/PriceController.kt +++ b/src/main/kotlin/ie/daithi/electricityprices/web/controller/PriceController.kt @@ -1,15 +1,25 @@ package ie.daithi.electricityprices.web.controller +import ie.daithi.electricityprices.exceptions.DataNotAvailableYetException +import ie.daithi.electricityprices.exceptions.UnprocessableEntityException import ie.daithi.electricityprices.model.DailyPriceInfo import ie.daithi.electricityprices.model.Price import ie.daithi.electricityprices.service.PriceService +import ie.daithi.electricityprices.web.validaton.ValidDateDay import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.ParameterIn +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +@Validated @RestController @RequestMapping("/api/v1") @Tag(name = "Price", description = "Endpoints that relate to electricity prices") @@ -41,11 +51,55 @@ class PriceController( summary = "Get price info", description = "Returns price info for the date provided. " + "If no date is provided the default is the current day. Dates should be given in a string form yyyy-MM-dd" ) + @Parameter( + `in` = ParameterIn.PATH, + name = "date", + schema = Schema(type = "string", pattern = "\\d{4}-\\d{2}-\\d{2}"), + description = "Address of the transaction origin", + required = true, + example = "2023-08-30" + ) @ApiResponses( - ApiResponse(responseCode = "200", description = "Request successful") + value = [ + ApiResponse(responseCode = "200", description = "Request successful"), + ApiResponse( + responseCode = "404", description = "Data not available yet", content = [Content( + mediaType = "application/json", + schema = Schema(implementation = DataNotAvailableYetException::class), + examples = [ + ExampleObject( + value = "{\n" + + " \"timestamp\": \"2023-09-10T10:03:38.111+00:00\",\n" + + " \"status\": 404,\n" + + " \"error\": \"Not Found\",\n" + + " \"message\": \"No data available for 2024-01-01\",\n" + + " \"path\": \"/api/v1/price/dailyinfo/2024-01-01\"\n" + + "}" + ) + ] + )] + ), + ApiResponse( + responseCode = "422", description = "Invalid date", content = [Content( + mediaType = "application/json", + schema = Schema(implementation = UnprocessableEntityException::class), + examples = [ + ExampleObject( + value = "{\n" + + " \"timestamp\": \"2023-09-10T10:03:38.111+00:00\",\n" + + " \"status\": 422,\n" + + " \"error\": \"Unprocessable Entity\",\n" + + " \"message\": \"\"getDailyPriceInfo.date: The provided date is invalid. It must match yyyy-MM-dd\",\n" + + " \"path\": \"/api/v1/price/dailyinfo/incorrect\"\n" + + "}" + ) + ] + )] + ) + ] ) @ResponseBody - fun getDailyPriceInfo(@PathVariable date: String?): DailyPriceInfo { - return priceSerice.getDailyPriceInfo(date) + fun getDailyPriceInfo(@ValidDateDay @PathVariable date: String): DailyPriceInfo { + return priceSerice.getDailyPriceInfo(date) ?: throw DataNotAvailableYetException("No data available for $date") } } \ No newline at end of file diff --git a/src/main/kotlin/ie/daithi/electricityprices/web/validaton/ValidDateDay.kt b/src/main/kotlin/ie/daithi/electricityprices/web/validaton/ValidDateDay.kt new file mode 100644 index 0000000..1ae510b --- /dev/null +++ b/src/main/kotlin/ie/daithi/electricityprices/web/validaton/ValidDateDay.kt @@ -0,0 +1,36 @@ +package ie.daithi.electricityprices.web.validaton + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import kotlin.reflect.KClass + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [DateDayValidator::class]) +@MustBeDocumented +annotation class ValidDateDay( + val message: String = + "The provided date is invalid. It must match yyyy-MM-dd", + val groups: Array> = [], + val payload: Array> = [] +) + +class DateDayValidator : ConstraintValidator { + override fun isValid( + value: String, + constraintValidatorContext: ConstraintValidatorContext + ): Boolean { + return try { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + LocalDate.parse(value, formatter) + true + } catch (e: DateTimeParseException) { + false + } + } +}