Skip to content

Commit

Permalink
feat: adding validators and error responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
daithihearn committed Sep 10, 2023
1 parent 9c2e3bd commit 8244f2a
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.2
1.12.0
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package ie.daithi.electricityprices.exceptions

class DataNotAvailableYetException(s: String) : Throwable()
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus


@ResponseStatus(value = HttpStatus.NOT_FOUND)
class DataNotAvailableYetException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -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<Map<String, Any>> {
val errorAttributes = mutableMapOf<String, Any>()
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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)

class DateDayValidator : ConstraintValidator<ValidDateDay, String> {
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
}
}
}

0 comments on commit 8244f2a

Please sign in to comment.