add openapi auto generation

some responses refactoring
This commit is contained in:
Denis Savosin
2024-10-03 12:41:33 +07:00
parent e89c1d99fb
commit 9ded10a9ac
11 changed files with 80 additions and 29 deletions

View File

@@ -24,11 +24,13 @@ repositories {
dependencies {
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.jetbrains.kotlin:kotlin-reflect:2.0.20")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
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-validation:3.2.4")
implementation("org.springframework.boot:spring-boot-starter-web:3.2.4")

View File

@@ -3,11 +3,18 @@ package com.example.demo.http.controllers
import com.example.demo.http.exceptions.NotFoundException
import com.example.demo.http.exceptions.UnprocessableException
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.page.PageResponse
import com.example.demo.models.Product
import com.example.demo.services.database.exceptions.AlreadyDeletedException
import com.example.demo.services.database.product.ProductService
import com.example.demo.services.database.product.exceptions.ProductNotFoundException
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 org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.Pageable
@@ -24,6 +31,14 @@ class ProductController(
) {
@GetMapping("/{guid}")
@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(
@PathVariable guid: UUID,
): ResponseEntity<Any> {
@@ -33,19 +48,18 @@ class ProductController(
}
@GetMapping("")
@ApiResponses(value = [
ApiResponse(responseCode = "200", content = [
Content(mediaType = "application/json", schema = Schema(implementation = PageResponse::class)),
]),
])
fun getProducts(
@ParameterObject pageable: Pageable,
): ResponseEntity<Any> {
val products = productService.findAll(pageable)
return ResponseEntity(
mapOf(
"data" to products.content,
"meta" to mapOf(
"total" to products.totalElements,
"pages" to products.totalPages,
),
),
PageResponse(products),
HttpStatus.OK,
)
}

View File

@@ -1,15 +1,15 @@
package com.example.demo.http.exceptions
import com.example.demo.http.responses.makeBadRequestResponse
import com.example.demo.http.responses.makeNotFoundResponse
import com.example.demo.http.responses.makeUnprocessableResponse
import com.example.demo.http.responses.makeUnprocessableResponseWithErrors
import com.example.demo.http.responses.BadRequestResponse
import com.example.demo.http.responses.NotFoundResponse
import com.example.demo.http.responses.UnprocessableResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
@ControllerAdvice
class ExceptionHandler {
@@ -17,25 +17,29 @@ class ExceptionHandler {
// 400
@ExceptionHandler(HttpMessageNotReadableException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleMessageNotReadable(exception: HttpMessageNotReadableException): ResponseEntity<Any> = ResponseEntity(
makeBadRequestResponse(exception.message.toString()),
BadRequestResponse(exception.message.toString()),
HttpStatus.BAD_REQUEST,
)
// 404
@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
@ExceptionHandler(UnprocessableException::class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<Any> = ResponseEntity(
makeUnprocessableResponse(exception.message),
UnprocessableResponse(exception.message),
HttpStatus.UNPROCESSABLE_ENTITY,
)
@ExceptionHandler(MethodArgumentNotValidException::class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
fun handleMethodArgumentNotValid(exception: MethodArgumentNotValidException): ResponseEntity<Any> = ResponseEntity(
makeUnprocessableResponseWithErrors(exception.javaClass.name, exception.allErrors),
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
HttpStatus.UNPROCESSABLE_ENTITY,
)
}

View File

@@ -3,5 +3,3 @@ package com.example.demo.http.responses
data class BadRequestResponse(
val cause: String,
): BaseResponse(status = ResponseStatus.BAD_REQUEST)
fun makeBadRequestResponse(cause: String): BadRequestResponse = BadRequestResponse(cause)

View File

@@ -3,5 +3,3 @@ package com.example.demo.http.responses
open class BaseResponse(val status: ResponseStatus)
fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatus.OK)
fun makeNotFoundResponse(): BaseResponse = BaseResponse(status = ResponseStatus.NOT_FOUND)

View File

@@ -0,0 +1,3 @@
package com.example.demo.http.responses
class NotFoundResponse: BaseResponse(ResponseStatus.NOT_FOUND)

View File

@@ -4,10 +4,7 @@ import org.springframework.validation.ObjectError
class UnprocessableResponse(
val cause: String,
val errors: List<ObjectError>? = null
): BaseResponse(status = ResponseStatus.UNPROCESSABLE)
fun makeUnprocessableResponse(cause: String): UnprocessableResponse = UnprocessableResponse(cause)
fun makeUnprocessableResponseWithErrors(
cause: String, errors: List<ObjectError>,
): UnprocessableResponse = UnprocessableResponse(cause, errors)
val errors: List<ObjectError>?
): BaseResponse(status = ResponseStatus.UNPROCESSABLE) {
constructor(cause: String): this(cause, null)
}

View File

@@ -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,
)

View File

@@ -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
)
)
}

View File

@@ -24,3 +24,9 @@ kafka:
group-id: demo-consumer
topics: demo-city-sync
auto-startup: true
springdoc:
api-docs:
path: /doc/openapi
swagger-ui:
path: /doc/swagger-ui.html

View File

@@ -83,8 +83,10 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
@Test
fun getProducts_success() {
val now = OffsetDateTime.now()
val pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt"))
whenever(productService.findAll(
PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt")),
pageRequest,
)) doReturn PageImpl(listOf(Product(
id = 12,
guid = UUID.randomUUID(),
@@ -96,7 +98,7 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
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 { content { contentType(MediaType.APPLICATION_JSON) } }
.andExpect { jsonPath("\$.meta.total") { value(1) } }