mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-25 16:22:35 +03:00
add openapi auto generation
some responses refactoring
This commit is contained in:
@@ -24,11 +24,13 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api("org.springframework.boot:spring-boot-starter-data-jdbc:3.2.4")
|
api("org.springframework.boot:spring-boot-starter-data-jdbc:3.2.4")
|
||||||
|
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.4")
|
||||||
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.4")
|
||||||
implementation("org.flywaydb:flyway-core:9.22.3")
|
implementation("org.flywaydb:flyway-core:9.22.3")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.20")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.20")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||||
implementation("org.postgresql:postgresql:42.6.2")
|
implementation("org.postgresql:postgresql:42.6.2")
|
||||||
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mustache:3.2.4")
|
implementation("org.springframework.boot:spring-boot-starter-mustache:3.2.4")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation:3.2.4")
|
implementation("org.springframework.boot:spring-boot-starter-validation:3.2.4")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web:3.2.4")
|
implementation("org.springframework.boot:spring-boot-starter-web:3.2.4")
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ package com.example.demo.http.controllers
|
|||||||
import com.example.demo.http.exceptions.NotFoundException
|
import com.example.demo.http.exceptions.NotFoundException
|
||||||
import com.example.demo.http.exceptions.UnprocessableException
|
import com.example.demo.http.exceptions.UnprocessableException
|
||||||
import com.example.demo.http.requests.CreateProductRequest
|
import com.example.demo.http.requests.CreateProductRequest
|
||||||
|
import com.example.demo.http.responses.NotFoundResponse
|
||||||
import com.example.demo.http.responses.makeOkResponse
|
import com.example.demo.http.responses.makeOkResponse
|
||||||
|
import com.example.demo.http.responses.page.PageResponse
|
||||||
|
import com.example.demo.models.Product
|
||||||
import com.example.demo.services.database.exceptions.AlreadyDeletedException
|
import com.example.demo.services.database.exceptions.AlreadyDeletedException
|
||||||
import com.example.demo.services.database.product.ProductService
|
import com.example.demo.services.database.product.ProductService
|
||||||
import com.example.demo.services.database.product.exceptions.ProductNotFoundException
|
import com.example.demo.services.database.product.exceptions.ProductNotFoundException
|
||||||
import com.example.demo.services.kafka.exceptions.InvalidArgumentException
|
import com.example.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content
|
||||||
|
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 jakarta.validation.Valid
|
import jakarta.validation.Valid
|
||||||
import org.springdoc.core.annotations.ParameterObject
|
import org.springdoc.core.annotations.ParameterObject
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
@@ -24,6 +31,14 @@ class ProductController(
|
|||||||
) {
|
) {
|
||||||
@GetMapping("/{guid}")
|
@GetMapping("/{guid}")
|
||||||
@Throws(NotFoundException::class)
|
@Throws(NotFoundException::class)
|
||||||
|
@ApiResponses(value = [
|
||||||
|
ApiResponse(responseCode = "200", content = [
|
||||||
|
Content(mediaType = "application/json", schema = Schema(implementation = Product::class)),
|
||||||
|
]),
|
||||||
|
ApiResponse(responseCode = "404", content = [
|
||||||
|
Content(mediaType = "application/json", schema = Schema(implementation = NotFoundResponse::class))
|
||||||
|
])
|
||||||
|
])
|
||||||
fun getProduct(
|
fun getProduct(
|
||||||
@PathVariable guid: UUID,
|
@PathVariable guid: UUID,
|
||||||
): ResponseEntity<Any> {
|
): ResponseEntity<Any> {
|
||||||
@@ -33,19 +48,18 @@ class ProductController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("")
|
@GetMapping("")
|
||||||
|
@ApiResponses(value = [
|
||||||
|
ApiResponse(responseCode = "200", content = [
|
||||||
|
Content(mediaType = "application/json", schema = Schema(implementation = PageResponse::class)),
|
||||||
|
]),
|
||||||
|
])
|
||||||
fun getProducts(
|
fun getProducts(
|
||||||
@ParameterObject pageable: Pageable,
|
@ParameterObject pageable: Pageable,
|
||||||
): ResponseEntity<Any> {
|
): ResponseEntity<Any> {
|
||||||
val products = productService.findAll(pageable)
|
val products = productService.findAll(pageable)
|
||||||
|
|
||||||
return ResponseEntity(
|
return ResponseEntity(
|
||||||
mapOf(
|
PageResponse(products),
|
||||||
"data" to products.content,
|
|
||||||
"meta" to mapOf(
|
|
||||||
"total" to products.totalElements,
|
|
||||||
"pages" to products.totalPages,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
HttpStatus.OK,
|
HttpStatus.OK,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.example.demo.http.exceptions
|
package com.example.demo.http.exceptions
|
||||||
|
|
||||||
import com.example.demo.http.responses.makeBadRequestResponse
|
import com.example.demo.http.responses.BadRequestResponse
|
||||||
import com.example.demo.http.responses.makeNotFoundResponse
|
import com.example.demo.http.responses.NotFoundResponse
|
||||||
import com.example.demo.http.responses.makeUnprocessableResponse
|
import com.example.demo.http.responses.UnprocessableResponse
|
||||||
import com.example.demo.http.responses.makeUnprocessableResponseWithErrors
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
class ExceptionHandler {
|
class ExceptionHandler {
|
||||||
@@ -17,25 +17,29 @@ class ExceptionHandler {
|
|||||||
|
|
||||||
// 400
|
// 400
|
||||||
@ExceptionHandler(HttpMessageNotReadableException::class)
|
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
fun handleMessageNotReadable(exception: HttpMessageNotReadableException): ResponseEntity<Any> = ResponseEntity(
|
fun handleMessageNotReadable(exception: HttpMessageNotReadableException): ResponseEntity<Any> = ResponseEntity(
|
||||||
makeBadRequestResponse(exception.message.toString()),
|
BadRequestResponse(exception.message.toString()),
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
@ExceptionHandler(NotFoundException::class)
|
@ExceptionHandler(NotFoundException::class)
|
||||||
fun handleNotFound(): ResponseEntity<Any> = ResponseEntity(makeNotFoundResponse(), HttpStatus.NOT_FOUND)
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
fun handleNotFound(): ResponseEntity<Any> = ResponseEntity(NotFoundResponse(), HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
// 422
|
// 422
|
||||||
@ExceptionHandler(UnprocessableException::class)
|
@ExceptionHandler(UnprocessableException::class)
|
||||||
|
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<Any> = ResponseEntity(
|
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<Any> = ResponseEntity(
|
||||||
makeUnprocessableResponse(exception.message),
|
UnprocessableResponse(exception.message),
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException::class)
|
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||||
|
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
fun handleMethodArgumentNotValid(exception: MethodArgumentNotValidException): ResponseEntity<Any> = ResponseEntity(
|
fun handleMethodArgumentNotValid(exception: MethodArgumentNotValidException): ResponseEntity<Any> = ResponseEntity(
|
||||||
makeUnprocessableResponseWithErrors(exception.javaClass.name, exception.allErrors),
|
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,3 @@ package com.example.demo.http.responses
|
|||||||
data class BadRequestResponse(
|
data class BadRequestResponse(
|
||||||
val cause: String,
|
val cause: String,
|
||||||
): BaseResponse(status = ResponseStatus.BAD_REQUEST)
|
): BaseResponse(status = ResponseStatus.BAD_REQUEST)
|
||||||
|
|
||||||
fun makeBadRequestResponse(cause: String): BadRequestResponse = BadRequestResponse(cause)
|
|
||||||
|
|||||||
@@ -3,5 +3,3 @@ package com.example.demo.http.responses
|
|||||||
open class BaseResponse(val status: ResponseStatus)
|
open class BaseResponse(val status: ResponseStatus)
|
||||||
|
|
||||||
fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatus.OK)
|
fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatus.OK)
|
||||||
|
|
||||||
fun makeNotFoundResponse(): BaseResponse = BaseResponse(status = ResponseStatus.NOT_FOUND)
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.example.demo.http.responses
|
||||||
|
|
||||||
|
class NotFoundResponse: BaseResponse(ResponseStatus.NOT_FOUND)
|
||||||
@@ -4,10 +4,7 @@ import org.springframework.validation.ObjectError
|
|||||||
|
|
||||||
class UnprocessableResponse(
|
class UnprocessableResponse(
|
||||||
val cause: String,
|
val cause: String,
|
||||||
val errors: List<ObjectError>? = null
|
val errors: List<ObjectError>?
|
||||||
): BaseResponse(status = ResponseStatus.UNPROCESSABLE)
|
): BaseResponse(status = ResponseStatus.UNPROCESSABLE) {
|
||||||
|
constructor(cause: String): this(cause, null)
|
||||||
fun makeUnprocessableResponse(cause: String): UnprocessableResponse = UnprocessableResponse(cause)
|
}
|
||||||
fun makeUnprocessableResponseWithErrors(
|
|
||||||
cause: String, errors: List<ObjectError>,
|
|
||||||
): UnprocessableResponse = UnprocessableResponse(cause, errors)
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.example.demo.http.responses.page
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageMetaDto(
|
||||||
|
val total: Long,
|
||||||
|
val pages: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.example.demo.http.responses.page
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.springframework.data.domain.Page
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageResponse<T>(
|
||||||
|
val data: List<T>,
|
||||||
|
val meta: PageMetaDto,
|
||||||
|
) {
|
||||||
|
constructor(page: Page<T>) : this(
|
||||||
|
data = page.content,
|
||||||
|
meta = PageMetaDto(
|
||||||
|
total = page.totalElements,
|
||||||
|
pages = page.totalPages
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,3 +24,9 @@ kafka:
|
|||||||
group-id: demo-consumer
|
group-id: demo-consumer
|
||||||
topics: demo-city-sync
|
topics: demo-city-sync
|
||||||
auto-startup: true
|
auto-startup: true
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /doc/openapi
|
||||||
|
swagger-ui:
|
||||||
|
path: /doc/swagger-ui.html
|
||||||
@@ -83,8 +83,10 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun getProducts_success() {
|
fun getProducts_success() {
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
|
val pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||||
|
|
||||||
whenever(productService.findAll(
|
whenever(productService.findAll(
|
||||||
PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt")),
|
pageRequest,
|
||||||
)) doReturn PageImpl(listOf(Product(
|
)) doReturn PageImpl(listOf(Product(
|
||||||
id = 12,
|
id = 12,
|
||||||
guid = UUID.randomUUID(),
|
guid = UUID.randomUUID(),
|
||||||
@@ -96,7 +98,7 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
deletedAt = null,
|
deletedAt = null,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
mockMvc.get("/api/product?page=1&size=2&sort=created_at,desc")
|
mockMvc.get("/api/product?page=1&size=2&sort=createdAt,desc")
|
||||||
.andExpect { status { status { isOk() } } }
|
.andExpect { status { status { isOk() } } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||||
.andExpect { jsonPath("\$.meta.total") { value(1) } }
|
.andExpect { jsonPath("\$.meta.total") { value(1) } }
|
||||||
|
|||||||
Reference in New Issue
Block a user