From 27595e08dc15977178b9882cf720549cd9eebf39 Mon Sep 17 00:00:00 2001 From: Denis Savosin Date: Mon, 30 Sep 2024 12:41:29 +0700 Subject: [PATCH] add new api methods to product controller --- doc/examples/api/product.md | 16 +++++++ src/main/kotlin/com/example/demo/AppConfig.kt | 11 ++++- .../demo/controllers/ProductController.kt | 46 +++++++++++-------- .../demo/exceptions/ExceptionHandler.kt | 11 ++++- .../demo/exceptions/NotFoundException.kt | 2 +- .../demo/provider/MockedShopProvider.kt | 1 + .../example/demo/responses/BaseResponse.kt | 6 ++- .../example/demo/responses/ResponseStatus.kt | 4 +- .../demo/responses/UnprocessableResponse.kt | 7 +++ .../demo/controllers/ProductControllerTest.kt | 9 ++-- .../demo/controllers/ShopControllerTest.kt | 3 +- 11 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 doc/examples/api/product.md create mode 100644 src/main/kotlin/com/example/demo/responses/UnprocessableResponse.kt diff --git a/doc/examples/api/product.md b/doc/examples/api/product.md new file mode 100644 index 0000000..0598a8b --- /dev/null +++ b/doc/examples/api/product.md @@ -0,0 +1,16 @@ +```shell +curl --request GET \ + --url 'http://localhost:8080/api/product/179cffdc-90f8-4627-985d-3d9c88dff5d7' +``` + +```shell +curl --request POST \ + --url http://localhost:8080/api/product \ + -H "Content-Type: application/json" \ + -d '{"name":"product-tree","description":"some other product","price":30000}' +``` + +```shell +curl --request DELETE \ + --url 'http://localhost:8080/api/product/179cffdc-90f8-4627-985d-3d9c88dff5d7' +``` diff --git a/src/main/kotlin/com/example/demo/AppConfig.kt b/src/main/kotlin/com/example/demo/AppConfig.kt index ed9d098..e0e091f 100644 --- a/src/main/kotlin/com/example/demo/AppConfig.kt +++ b/src/main/kotlin/com/example/demo/AppConfig.kt @@ -1,16 +1,23 @@ package com.example.demo import com.example.demo.provider.MockedShopProvider +import com.example.demo.provider.ProductRepository import com.example.demo.provider.ShopProvider +import com.example.demo.services.ProductService +import com.example.demo.services.ProductServiceImpl +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.data.jpa.repository.config.EnableJpaRepositories @Configuration -@EnableJpaRepositories(basePackages = ["com.example.demo.providers"]) class AppConfig { @Bean fun shopProvider(): ShopProvider{ return MockedShopProvider() } + + @Bean + fun productService(@Autowired productRepository: ProductRepository): ProductService { + return ProductServiceImpl(productRepository = productRepository) + } } diff --git a/src/main/kotlin/com/example/demo/controllers/ProductController.kt b/src/main/kotlin/com/example/demo/controllers/ProductController.kt index 0342ca7..b01c66c 100644 --- a/src/main/kotlin/com/example/demo/controllers/ProductController.kt +++ b/src/main/kotlin/com/example/demo/controllers/ProductController.kt @@ -1,50 +1,56 @@ package com.example.demo.controllers import com.example.demo.exceptions.NotFoundException -import com.example.demo.models.Product -import com.example.demo.provider.ProductRepository +import com.example.demo.exceptions.UnprocessableException import com.example.demo.requests.CreateProductRequest +import com.example.demo.responses.makeOkResponse +import com.example.demo.services.ProductService import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToJsonElement -import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* -import java.time.OffsetDateTime import java.util.* @RestController -@RequestMapping(value = ["/api/product"]) +@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE]) class ProductController( - @Autowired val productRepository: ProductRepository + val productService: ProductService ) { - @GetMapping(value = ["{guid}"], produces = ["application/json"]) + @GetMapping("/{guid}") @ResponseBody + @Throws(NotFoundException::class) fun getProduct( @PathVariable guid: UUID ): String { - val product = productRepository.findByGuid(guid = guid) ?: throw NotFoundException() + val product = productService.findByGuid(guid = guid) ?: throw NotFoundException() return Json.encodeToJsonElement(value = product).toString() } - @PostMapping(value = [""], consumes = ["application/json"], produces = ["application/json"]) + @PostMapping(value = ["/"], consumes = [MediaType.APPLICATION_JSON_VALUE]) @ResponseBody fun createProduct( @RequestBody product: CreateProductRequest ): String { - val productModel = Product( - id = null, - guid = UUID.randomUUID(), - name = product.name, - description = product.description, - price = product.price, - createdAt = OffsetDateTime.now(), - updatedAt = null, + val saved = productService.create( + product.name, + product.price, + product.description, ) - val saved = productRepository.save(productModel) - return Json.encodeToJsonElement(value = saved).toString() } - // todo delete with soft-delete + @DeleteMapping("/{guid}") + @ResponseBody + @Throws(NotFoundException::class, UnprocessableException::class) + fun deleteProduct( + @PathVariable guid: UUID, + ): ResponseEntity { + productService.delete(guid) + + return ResponseEntity(makeOkResponse(), HttpStatus.OK) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/exceptions/ExceptionHandler.kt b/src/main/kotlin/com/example/demo/exceptions/ExceptionHandler.kt index 1ea752a..be26535 100644 --- a/src/main/kotlin/com/example/demo/exceptions/ExceptionHandler.kt +++ b/src/main/kotlin/com/example/demo/exceptions/ExceptionHandler.kt @@ -1,6 +1,7 @@ package com.example.demo.exceptions -import com.example.demo.responses.makeNotFound +import com.example.demo.responses.makeNotFoundResponse +import com.example.demo.responses.makeUnprocessableResponse import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ControllerAdvice @@ -9,5 +10,11 @@ import org.springframework.web.bind.annotation.ExceptionHandler @ControllerAdvice class ExceptionHandler { @ExceptionHandler(NotFoundException::class) - fun handleNotFound(): ResponseEntity = ResponseEntity(makeNotFound(), HttpStatus.NOT_FOUND) + fun handleNotFound(): ResponseEntity = ResponseEntity(makeNotFoundResponse(), HttpStatus.NOT_FOUND) + + @ExceptionHandler(UnprocessableException::class) + fun handleUnprocessable(exception: UnprocessableException): ResponseEntity = ResponseEntity( + makeUnprocessableResponse(exception.message), + HttpStatus.UNPROCESSABLE_ENTITY, + ) } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/exceptions/NotFoundException.kt b/src/main/kotlin/com/example/demo/exceptions/NotFoundException.kt index ec49227..4cc0fd6 100644 --- a/src/main/kotlin/com/example/demo/exceptions/NotFoundException.kt +++ b/src/main/kotlin/com/example/demo/exceptions/NotFoundException.kt @@ -1,3 +1,3 @@ package com.example.demo.exceptions -class NotFoundException: RuntimeException() \ No newline at end of file +class NotFoundException: RuntimeException() diff --git a/src/main/kotlin/com/example/demo/provider/MockedShopProvider.kt b/src/main/kotlin/com/example/demo/provider/MockedShopProvider.kt index ddc459b..2c2bd53 100644 --- a/src/main/kotlin/com/example/demo/provider/MockedShopProvider.kt +++ b/src/main/kotlin/com/example/demo/provider/MockedShopProvider.kt @@ -41,6 +41,7 @@ class MockedShopProvider: ShopProvider { price = (price * 100).toLong(), createdAt = OffsetDateTime.now(), updatedAt = null, + deletedAt = null, ) } } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/responses/BaseResponse.kt b/src/main/kotlin/com/example/demo/responses/BaseResponse.kt index 8942bde..d03e341 100644 --- a/src/main/kotlin/com/example/demo/responses/BaseResponse.kt +++ b/src/main/kotlin/com/example/demo/responses/BaseResponse.kt @@ -1,5 +1,7 @@ package com.example.demo.responses -class BaseResponse(val status: ResponseStatus) +open class BaseResponse(val status: ResponseStatus) -fun makeNotFound(): BaseResponse = BaseResponse(status = ResponseStatus.NOT_FOUND) \ No newline at end of file +fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatus.OK) + +fun makeNotFoundResponse(): BaseResponse = BaseResponse(status = ResponseStatus.NOT_FOUND) diff --git a/src/main/kotlin/com/example/demo/responses/ResponseStatus.kt b/src/main/kotlin/com/example/demo/responses/ResponseStatus.kt index 8da1b1a..e7f76c8 100644 --- a/src/main/kotlin/com/example/demo/responses/ResponseStatus.kt +++ b/src/main/kotlin/com/example/demo/responses/ResponseStatus.kt @@ -3,5 +3,7 @@ package com.example.demo.responses import com.fasterxml.jackson.annotation.JsonValue enum class ResponseStatus(@JsonValue val status: String) { - NOT_FOUND("not found"); + OK("ok"), + NOT_FOUND("not found"), + UNPROCESSABLE("unprocessable"); } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/responses/UnprocessableResponse.kt b/src/main/kotlin/com/example/demo/responses/UnprocessableResponse.kt new file mode 100644 index 0000000..39075e3 --- /dev/null +++ b/src/main/kotlin/com/example/demo/responses/UnprocessableResponse.kt @@ -0,0 +1,7 @@ +package com.example.demo.responses + +class UnprocessableResponse( + val cause: String, +): BaseResponse(status = ResponseStatus.UNPROCESSABLE) + +fun makeUnprocessableResponse(cause: String): UnprocessableResponse = UnprocessableResponse(cause) \ No newline at end of file diff --git a/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt b/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt index 511f079..4c53d69 100644 --- a/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt +++ b/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt @@ -1,8 +1,8 @@ package com.example.demo.controllers import com.example.demo.models.Product -import com.example.demo.provider.ProductRepository import com.example.demo.responses.ResponseStatus +import com.example.demo.services.ProductService import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.Test import org.mockito.kotlin.doReturn @@ -20,7 +20,7 @@ import java.util.* @WebMvcTest(ProductController::class) class ProductControllerTest(@Autowired val mockMvc: MockMvc) { @MockBean - private lateinit var productRepository: ProductRepository + private lateinit var productService: ProductService @Test fun getProduct_success() { @@ -34,9 +34,10 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { price = 11130, createdAt = now, updatedAt = null, + deletedAt = null, ) - whenever(productRepository.findByGuid( + whenever(productService.findByGuid( eq(guid), )) doReturn product @@ -54,7 +55,7 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { fun getProduct_notFound() { val guid = UUID.randomUUID() - whenever(productRepository.findByGuid( + whenever(productService.findByGuid( eq(guid), )) doReturn null diff --git a/src/test/kotlin/com/example/demo/controllers/ShopControllerTest.kt b/src/test/kotlin/com/example/demo/controllers/ShopControllerTest.kt index 301d867..c7f67ba 100644 --- a/src/test/kotlin/com/example/demo/controllers/ShopControllerTest.kt +++ b/src/test/kotlin/com/example/demo/controllers/ShopControllerTest.kt @@ -81,9 +81,10 @@ class ShopControllerTest(@Autowired val mockMvc: MockMvc) { guid = UUID.randomUUID(), name = name, description = null, - price = (price * 100).toInt(), + price = (price * 100).toLong(), createdAt = OffsetDateTime.now(), updatedAt = null, + deletedAt = null, ) } } \ No newline at end of file