Skip to content

Commit

Permalink
Tacs 19 scheduler entry points (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasanchez committed Apr 22, 2023
2 parents 052f497 + ededd0d commit d2628ec
Show file tree
Hide file tree
Showing 23 changed files with 1,215 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/assets/schedule-aggregate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.schedutn.scheduler.adapters

import com.schedutn.scheduler.domain.models.Schedule
import java.util.*

class ScheduleRepository(
private val db: MutableMap<String, Schedule> = mutableMapOf()
) {

fun save(schedule: Schedule): Schedule {

val id: String = schedule.id ?: UUID.randomUUID().toString()

val saved = schedule.copy(id = id, version = schedule.version + 1)

if (schedule.id == null) {
db[id] = saved
return saved
}

val current = db[id]!!

if (schedule.version >= current.version) {
db[id] = saved
return saved
}

throw IllegalStateException("Schedule with id $id is not up to date")
}

fun findById(id: String): Schedule? {
return db[id]
}

fun findAll(): Collection<Schedule> {
return db.values
}

fun deleteById(id: String) {
db.remove(id)
}
}
28 changes: 28 additions & 0 deletions scheduler/src/main/java/com/schedutn/scheduler/api/DataWrapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.schedutn.scheduler.api

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.validation.annotation.Validated

@Schema(description = "Data wrapper")
@JsonInclude(JsonInclude.Include.NON_NULL)
class DataWrapper<T>(

@Schema(description = "Response status", example = "success")
@JsonProperty("status")
val status: String? = "success",

@Schema(description = "Message of the response", example = "OK")
@JsonProperty("message")
val message: String? = null,

@Schema(description = "Data of the response")
@JsonProperty("data")
@Validated
val data: T? = null,

@Schema(description = "Metadata of the response", example = "{ \"total\": 1 }")
@JsonProperty("meta")
val meta: Map<String, Any>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.schedutn.scheduler.api

import com.schedutn.scheduler.api.errors.InvalidSchedule
import com.schedutn.scheduler.api.errors.UnAuthorizedScheduleOperation
import com.schedutn.scheduler.service.ScheduleAuthorizationException
import com.schedutn.scheduler.service.ScheduleNotFoundException
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.validation.ObjectError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler

@RestControllerAdvice
class GlobalControllerExceptionHandler : ResponseEntityExceptionHandler() {

companion object {

private val log = org.slf4j.LoggerFactory.getLogger(
GlobalControllerExceptionHandler::class.java)
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(ScheduleNotFoundException::class)
fun handleScheduleNotFound(
ex: ScheduleNotFoundException): ResponseEntity<InvalidSchedule> {

val details = InvalidSchedule(
code = "404",
message = ex.message ?: "Not found"
)

log.error("404: $details")
return ResponseEntity(details, HttpStatus.NOT_FOUND)
}

@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(ScheduleAuthorizationException::class)
fun handleScheduleAuthorization(
ex: ScheduleAuthorizationException): ResponseEntity<UnAuthorizedScheduleOperation> {

val bodyOfResponse = UnAuthorizedScheduleOperation(
code = "403",
message = ex.message ?: "Forbidden"
)

return ResponseEntity(bodyOfResponse, HttpStatus.FORBIDDEN)
}

/**
* Handles Unprocessable Entity Exceptions.
*
* @param ex The runtime exception
* @return a response entity with the occurred errors and an unprocessable entity status
*/
override fun handleMethodArgumentNotValid(ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest): ResponseEntity<Any> {

val errors: MutableMap<String, String?> = HashMap()

ex.bindingResult.allErrors.forEach { error: ObjectError ->
val fieldName = (error as FieldError).field
val message = error.getDefaultMessage()
errors[fieldName] = message
}

val bodyOfResponse = mapOf<String, Any?>(
"message" to "Cannot process Entity, please check the errors.",
"detail" to errors
)

log.error("Cannot process Entity, please check the errors.")

return ResponseEntity(bodyOfResponse, HttpStatus.UNPROCESSABLE_ENTITY)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.schedutn.scheduler.api.errors

import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "Error response")
data class InvalidSchedule(

@Schema(description = "Error code", example = "404")
@JsonProperty("code")
val code: String,

@Schema(description = "Error message", example = "Not found")
@JsonProperty("message")
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.schedutn.scheduler.api.errors

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "Error response")
data class UnAuthorizedScheduleOperation(

@Schema(description = "Error code", example = "403")
val code: String,

@Schema(description = "Error message",
example = "User has no permission to perform this operation")
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.schedutn.scheduler.api.v1


import com.schedutn.scheduler.api.DataWrapper
import com.schedutn.scheduler.api.v1.SchedulesEntryPoint.Companion.SCHEDULES_ENTRY_POINT_URL
import com.schedutn.scheduler.domain.commands.ScheduleMeeting
import com.schedutn.scheduler.domain.commands.ToggleVoting
import com.schedutn.scheduler.domain.commands.VoteForOption
import com.schedutn.scheduler.domain.events.MeetingScheduled
import com.schedutn.scheduler.service.ScheduleService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
import java.net.URI

@RestController
@RequestMapping(SCHEDULES_ENTRY_POINT_URL, produces = [MediaType.APPLICATION_JSON_VALUE])
@Tag(name = "Schedules", description = "Schedules Entry Point")
class SchedulesEntryPoint {

@Autowired
lateinit var service: ScheduleService

companion object {

const val SCHEDULES_ENTRY_POINT_URL = "/api/v1/schedules"
private val log = org.slf4j.LoggerFactory.getLogger(SchedulesEntryPoint::class.java)
}

@GetMapping
@ResponseStatus(org.springframework.http.HttpStatus.OK)
@Operation(
summary = "Query Schedules",
description = "Retrieves available schedules",
tags = ["Queries"]
)
fun querySchedules(): DataWrapper<Collection<MeetingScheduled>> {
log.info("Querying schedules")
return DataWrapper(data = service.findAll())
}

@GetMapping("/{id}")
@ResponseStatus(org.springframework.http.HttpStatus.OK)
@Operation(
summary = "Query Schedule",
description = "Retrieves a schedule by id",
tags = ["Queries"]
)
fun querySchedule(@PathVariable id: String): DataWrapper<MeetingScheduled> {
log.info("Querying schedule with id: $id")

val schedule = service.scheduleById(id)

return DataWrapper(data = schedule)
}

@PostMapping
@ResponseStatus(org.springframework.http.HttpStatus.CREATED)
@Operation(
summary = "Commands to Schedule a Meeting",
description = "Creates a new meeting proposal",
tags = ["Commands"]
)
fun scheduleMeeting(
@Valid @RequestBody command: ScheduleMeeting): ResponseEntity<DataWrapper<MeetingScheduled>> {
log.info("Scheduling meeting: $command")

val scheduled = service.scheduleMeeting(command)

val uri = URI.create(
ServletUriComponentsBuilder
.fromCurrentContextPath()
.path("$SCHEDULES_ENTRY_POINT_URL/{id}")
.buildAndExpand(scheduled.id)
.toUriString()
)

return ResponseEntity.created(uri)
.body(DataWrapper(data = scheduled))
}

@PatchMapping("/{id}/voting")
@ResponseStatus(org.springframework.http.HttpStatus.OK)
@Operation(
summary = "Commands to Toggle Voting",
description = "Enables or disables voting for a schedule",
tags = ["Commands"]
)
fun toggleVoting(@PathVariable id: String,
@Valid @RequestBody command: ToggleVoting
): DataWrapper<MeetingScheduled> {
log.info("Toggling voting for schedule with id: $id")

val schedule = service.toggleVoting(id, command)

return DataWrapper(data = schedule)
}

@PatchMapping("/{id}/options")
@ResponseStatus(org.springframework.http.HttpStatus.OK)
@Operation(
summary = "Commands to Vote for an Option",
description = "Adds or Revokes a vote for an option",
tags = ["Commands"]
)
fun voteForOption(@PathVariable id: String,
@Valid @RequestBody command: VoteForOption
): DataWrapper<MeetingScheduled> {
log.info("Voting for option for schedule with id: $id")

val schedule = service.voteForAnOption(id = id, command = command)
return DataWrapper(data = schedule)
}

@PostMapping("/{id}/relationships/guests")
@ResponseStatus(org.springframework.http.HttpStatus.OK)
@Operation(
summary = "Commands to Join a Meeting",
description = "Adds a guest to a meeting",
tags = ["Commands"]
)
fun joinMeeting(@PathVariable id: String): DataWrapper<MeetingScheduled> {
log.info("Joining meeting for schedule with id: $id")

val auth = SecurityContextHolder.getContext().authentication.principal.toString()

val joined = service.joinAMeeting(id = id, username = auth)

log.info("$auth joined meeting for schedule with id: $id")

return DataWrapper(data = joined)
}
}
18 changes: 18 additions & 0 deletions scheduler/src/main/java/com/schedutn/scheduler/domain/Message.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.schedutn.scheduler.domain

import java.io.Serializable

/**
* Message is an object that carries information from one part of a program to another.
*
* It is a means of communication between different components of a system, and it can be used to
* trigger actions or update the state of the system.
*
* They can be passed synchronously or asynchronously and may be sent between different threads,
* processes, or even across different systems.
*
* By using messages to communicate between components, the system becomes more modular and easier
* to maintain, as each component only needs to understand the message format and not the
* implementation details of other components.
*/
interface Message : Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.schedutn.scheduler.domain.commands

import com.schedutn.scheduler.domain.Message


/**
* A command represents an intent to change the state of the system, it is a message that requests
* some action to be taken.
*
* Commands are passed to command handlers, which interpret them and execute
* the corresponding actions to produce new events that update the system state.
*
* Commands should be immutable, and their properties should be as minimal as possible.
*
*/
interface Command : Message
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.schedutn.scheduler.domain.commands

import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import java.time.LocalDate

@Schema(description = "Command to propose a meeting option")
data class ProposeOption(

@Schema(description = "Proposed Date", required = true)
@JsonProperty("date")
val date: LocalDate,

@Schema(description = "Proposed Hour", required = true)
@JsonProperty("hour")
@field:Max(value = 23, message = "Hour must be between 0 and 23")
@field:Min(value = 0, message = "Hour must be between 0 and 23")
val hour: Int,

@Schema(description = "Proposed Minute", required = true)
@JsonProperty("minute")
@field:Max(value = 59, message = "Minute must be between 0 and 59")
@field:Min(value = 0, message = "Minute must be between 0 and 59")
val minute: Int,
) : Command
Loading

0 comments on commit d2628ec

Please sign in to comment.