Skip to content
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

Add GET API for fetching combine target frame #22

Merged
merged 6 commits into from
Dec 14, 2023
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ When the submodule is updated, to get the newest version of inserter you need to
]
}
```
- `GET /timetables/to-combine`: Fetch the vehicle schedule frame ID slated to be the target for combine,
considering the combine action with the staging vehicle schedule frame ID and target priority.
- Request params:
- `stagingVehicleScheduleFrameId` The ID of the staging vehicle schedule frame. Example: `"50f939b0-aac3-453a-b2f5-24c0cdf8ad21"`
- `targetPriority` The priority to which the staging timetables will be promoted. Example: `10`

Example response body:
```JSON
{
"toCombineTargetVehicleScheduleFrameId": "d3d0aea6-db3f-4421-b4eb-39cffe8835a8"
}
```
## Technical Documentation

jore4-timetables-api is a Spring Boot application written in Kotlin, which implements a REST API for accessing the timetables database and creating more complicated updates in one transaction than is possible with the graphQL interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class TimetablesController(

) {
@AssertTrue(message = "false")
fun isTargetPriorityValid(): Boolean = runCatching { TimetablesPriority.fromInt(targetPriority) }.isSuccess
fun isTargetPriorityValid(): Boolean = TimetablesPriority.fromInt(targetPriority) != null
}

data class CombineTimetablesResponseBody(
Expand All @@ -60,7 +60,7 @@ class TimetablesController(
LOGGER.debug { "Combine api, request: $requestBody" }
val combineResult = combineTimetablesService.combineTimetables(
requestBody.stagingVehicleScheduleFrameIds,
TimetablesPriority.fromInt(requestBody.targetPriority)
parseTargetPriority(requestBody.targetPriority)
)

return ResponseEntity
Expand All @@ -80,7 +80,7 @@ class TimetablesController(
LOGGER.debug { "Replace api, request: $requestBody" }
val replaceResult = replaceTimetablesService.replaceTimetables(
requestBody.stagingVehicleScheduleFrameIds,
TimetablesPriority.fromInt(requestBody.targetPriority)
parseTargetPriority(requestBody.targetPriority)
)

return ResponseEntity
Expand All @@ -92,6 +92,10 @@ class TimetablesController(
val toReplaceVehicleScheduleFrameIds: List<UUID>
)

data class ToCombineTimetablesResponseBody(
val toCombineTargetVehicleScheduleFrameId: UUID
)

class TargetPriorityParsingException(message: String, val targetPriority: Int) : RuntimeException(message)

@GetMapping("to-replace")
Expand All @@ -103,20 +107,38 @@ class TimetablesController(
): ResponseEntity<ToReplaceTimetablesResponseBody> {
LOGGER.info { "ToReplace api, stagingVehicleScheduleFrameId: $stagingVehicleScheduleFrameId, targetPriority: $targetPriority" }

val targetPriorityEnumResult = runCatching { TimetablesPriority.fromInt(targetPriority) }
if (targetPriorityEnumResult.isFailure) {
throw TargetPriorityParsingException("Failed to parse target priority", targetPriority)
}

val vehicleScheduleFrameIds = replaceTimetablesService.fetchVehicleScheduleFramesToReplace(
stagingVehicleScheduleFrameId,
targetPriorityEnumResult.getOrThrow()
parseTargetPriority(targetPriority)
).mapNotNull { it.vehicleScheduleFrameId }

return ResponseEntity.status(HttpStatus.OK)
.body(ToReplaceTimetablesResponseBody(toReplaceVehicleScheduleFrameIds = vehicleScheduleFrameIds))
}

@GetMapping("to-combine")
fun getTargetFrameIdsForCombine(
@RequestParam
targetPriority: Int,
@RequestParam
stagingVehicleScheduleFrameId: UUID
): ResponseEntity<ToCombineTimetablesResponseBody> {
LOGGER.info { "ToCombine api, stagingVehicleScheduleFrameId: $stagingVehicleScheduleFrameId, targetPriority: $targetPriority" }

val targetVehicleScheduleFrame = combineTimetablesService.fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrameId,
parseTargetPriority(targetPriority)
)

return ResponseEntity.status(HttpStatus.OK)
.body(
ToCombineTimetablesResponseBody(
// ID of an existing row, can never be null.
toCombineTargetVehicleScheduleFrameId = targetVehicleScheduleFrame.vehicleScheduleFrameId!!
)
)
}

@ExceptionHandler(RuntimeException::class)
fun handleRuntimeException(ex: RuntimeException): ResponseEntity<JoreErrorResponse> {
val errorExtensions: JoreErrorExtensions = when (ex) {
Expand Down Expand Up @@ -155,4 +177,9 @@ class TimetablesController(

return ResponseEntity(JoreErrorResponse(ex.message, errorExtensions), httpStatus)
}

companion object {
private fun parseTargetPriority(targetPriority: Int) = TimetablesPriority.fromInt(targetPriority)
?: throw TargetPriorityParsingException("Failed to parse target priority", targetPriority)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class WebSecurityConfig {
HttpMethod.GET,
"/actuator/health",
"/error",
"/timetables/to-combine",
"/timetables/to-replace"
)
.permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ enum class TimetablesPriority(val value: Int) {
STAGING(40);

companion object {
fun fromInt(value: Int): TimetablesPriority {
return values().first { it.value == value }
fun fromInt(value: Int): TimetablesPriority? {
return values().firstOrNull { it.value == value }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ class VehicleScheduleFrameRepository(private val dsl: DSLContext, config: Defaul
val stagingFrameIdName: Name = DSL.name("stagingVehicleScheduleFrameId")
val replacedFrameIdName: Name = DSL.name("replacedVehicleScheduleFrameId")
val stagingVehicleScheduleFrameIdField = DSL.field(stagingFrameIdName, UUID::class.java)
val replacedVehicleScheduleFrameIdField =
DSL.field(replacedFrameIdName, UUID::class.java)
val replacedVehicleScheduleFrameIdField = DSL.field(replacedFrameIdName, UUID::class.java)

val stagingFrame = VEHICLE_SCHEDULE_FRAME.`as`("staging")
val replacedFrame = VEHICLE_SCHEDULE_FRAME.`as`("replaced")
Expand Down Expand Up @@ -102,8 +101,7 @@ class VehicleScheduleFrameRepository(private val dsl: DSLContext, config: Defaul
val stagingFrameIdName: Name = DSL.name("stagingVehicleScheduleFrameId")
val targetFrameIdName: Name = DSL.name("targetVehicleScheduleFrameId")
val stagingVehicleScheduleFrameIdField = DSL.field(stagingFrameIdName, UUID::class.java)
val targetVehicleScheduleFrameIdField =
DSL.field(targetFrameIdName, UUID::class.java)
val targetVehicleScheduleFrameIdField = DSL.field(targetFrameIdName, UUID::class.java)

val stagingFrame = VEHICLE_SCHEDULE_FRAME.`as`("staging")
val targetFrame = VEHICLE_SCHEDULE_FRAME.`as`("target")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ class CombineTimetablesService(

val stagingVehicleScheduleFrame = fetchStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId)

val targetVehicleScheduleFrame = fetchTargetVehicleScheduleFrame(stagingVehicleScheduleFrame, targetPriority)

LOGGER.info("Moving staging vehicle services to target...")
moveStagingVehicleServicesToTarget(
stagingFrame = stagingVehicleScheduleFrame,
targetFrame = targetVehicleScheduleFrame
)

LOGGER.info("Deleting the empty staging frame...")
deleteStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId)

return targetVehicleScheduleFrame.vehicleScheduleFrameId!! // ID of an existing row, can never be null.
}

@Transactional(readOnly = true)
fun fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrameId: UUID,
targetPriority: TimetablesPriority
): VehicleScheduleFrame {
val stagingVehicleScheduleFrame = fetchStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId)
return fetchTargetVehicleScheduleFrame(stagingVehicleScheduleFrame, targetPriority)
}

private fun fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrame: VehicleScheduleFrame,
targetPriority: TimetablesPriority
): VehicleScheduleFrame {
// ID of an existing row, can never be null.
val stagingVehicleScheduleFrameId = stagingVehicleScheduleFrame.vehicleScheduleFrameId!!

val targetVehicleScheduleFrames = vehicleScheduleFrameRepository
.fetchTargetVehicleScheduleFrames(stagingVehicleScheduleFrame, targetPriority)
LOGGER.info { "Found ${targetVehicleScheduleFrames.size} target vehicle schedule frames." }
Expand All @@ -61,19 +91,7 @@ class CombineTimetablesService(
)
}

// TODO: ensure that there are no identical journeys in staging and target.
// DB triggers do not ensure this.

LOGGER.info("Moving staging vehicle services to target...")
moveStagingVehicleServicesToTarget(
stagingFrame = stagingVehicleScheduleFrame,
targetFrame = targetVehicleScheduleFrame
)

LOGGER.info("Deleting the empty staging frame...")
deleteStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId)

return targetVehicleScheduleFrame.vehicleScheduleFrameId!! // ID of an existing row, can never be null.
return targetVehicleScheduleFrame
}

private fun fetchStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId: UUID): VehicleScheduleFrame {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fi.hsl.jore4.timetables.api

import com.ninjasquad.springmockk.MockkBean
import fi.hsl.jore.jore4.jooq.vehicle_schedule.tables.pojos.VehicleScheduleFrame
import fi.hsl.jore4.timetables.enumerated.TimetablesPriority
import fi.hsl.jore4.timetables.service.CombineTimetablesService
import io.mockk.every
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.LocalDate
import java.util.UUID

@ExtendWith(MockKExtension::class)
@AutoConfigureMockMvc
@SpringBootTest
@ActiveProfiles("test")
class TimetablesToCombineApiTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
private lateinit var combineTimetablesService: CombineTimetablesService

private val defaultTargetFrame = VehicleScheduleFrame(
vehicleScheduleFrameId = UUID.fromString("379076ee-d595-47e3-8050-2610d594b57c"),
validityStart = LocalDate.now(),
validityEnd = LocalDate.now(),
priority = 20,
label = "label"
)
private val defaultToCombineTargetId = defaultTargetFrame.vehicleScheduleFrameId

private fun executeToCombineTimetablesRequest(
stagingFrameId: UUID,
targetPriority: Int
): ResultActions {
return mockMvc.perform(
MockMvcRequestBuilders.get("/timetables/to-combine")
.contentType(MediaType.APPLICATION_JSON)
.param("stagingVehicleScheduleFrameId", stagingFrameId.toString())
.param("targetPriority", targetPriority.toString())
)
}

@Test
fun `returns 200 and correct response when called successfully`() {
val stagingVehicleScheduleFrameId = UUID.fromString("81f109d1-dbe2-412a-996e-aa510416b2e4")
val targetPriority = TimetablesPriority.STANDARD

every {
combineTimetablesService.fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrameId,
targetPriority
)
} answers { defaultTargetFrame }

executeToCombineTimetablesRequest(stagingVehicleScheduleFrameId, targetPriority.value)
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content().json(
"""
{
"toCombineTargetVehicleScheduleFrameId": $defaultToCombineTargetId
}
""".trimIndent(),
true
)
)

verify(exactly = 1) {
combineTimetablesService.fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrameId,
targetPriority
)
}
}

@Test
fun `throws a 400 error when parsing target priority fails`() {
val errorMessage = "Failed to parse target priority"
val stagingVehicleScheduleFrameId = UUID.fromString("023281cd-51e9-4544-a2af-7b7e268e3a3a")
val invalidTargetPriorityInput = 9999

executeToCombineTimetablesRequest(stagingVehicleScheduleFrameId, invalidTargetPriorityInput)
.andExpect(status().isBadRequest)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
content().json(
"""
{
"message": "$errorMessage",
"extensions": {
"code": 400,
"type": "TargetPriorityParsingError",
"targetPriority": $invalidTargetPriorityInput
}
}
""".trimIndent(),
true
)
)
verify(exactly = 0) {
combineTimetablesService.fetchTargetVehicleScheduleFrame(
stagingVehicleScheduleFrameId,
any()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ class TimetablesToReplaceApiTest(@Autowired val mockMvc: MockMvc) {
@Test
fun `returns 200 and correct response when called successfully`() {
val stagingVehicleScheduleFrameId = UUID.fromString("9e758776-2af1-49c7-8bd0-c0805b833b20")
val targetPriority = 10
val targetPriority = TimetablesPriority.STANDARD

every {
replaceTimetablesService.fetchVehicleScheduleFramesToReplace(
stagingVehicleScheduleFrameId,
TimetablesPriority.fromInt(targetPriority)
targetPriority
)
} answers { defaultVehicleScheduleFrames }

executeToReplaceTimetablesRequest(stagingVehicleScheduleFrameId, targetPriority)
executeToReplaceTimetablesRequest(stagingVehicleScheduleFrameId, targetPriority.value)
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(
Expand All @@ -88,7 +88,7 @@ class TimetablesToReplaceApiTest(@Autowired val mockMvc: MockMvc) {
verify(exactly = 1) {
replaceTimetablesService.fetchVehicleScheduleFramesToReplace(
stagingVehicleScheduleFrameId,
TimetablesPriority.fromInt(targetPriority)
targetPriority
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import kotlin.test.assertEquals

class TransactionSystemExtensionsTest {

private fun createTransactionSystemExceptionWithCause(message: String): TransactionSystemException {
return TransactionSystemException("test exception", Exception(message))
}
private fun createTransactionSystemExceptionWithCause(message: String) =
TransactionSystemException("test exception", Exception(message))

@Test
fun `resolves PassingTimeStopPointMatchingOrderError`() {
Expand Down
Loading
Loading