diff --git a/.gitignore b/.gitignore index 751d638..fb2c9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ lib/* *.iml *.idea -.vscode/* \ No newline at end of file +.vscode/* +.specmatic \ No newline at end of file diff --git a/pom.xml b/pom.xml index 021c1e2..81ddb3f 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 1.9.24 1.8 - 0.81.0 + 1.3.25 2.7.18 @@ -31,6 +31,12 @@ org.springframework.boot spring-boot-starter ${spring.boot.version} + + + org.yaml + snakeyaml + + @@ -121,13 +127,6 @@ 3.8.1 - - in.specmatic - specmatic-core - ${specmatic.version} - test - - in.specmatic junit5-support diff --git a/specmatic.json b/specmatic.json deleted file mode 100644 index c325808..0000000 --- a/specmatic.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "sources": [ - { - "provider": "git", - "repository": "https://github.com/znsio/specmatic-order-contracts.git", - "test": [ - "in/specmatic/examples/store/api_order_with_oauth_v1.yaml" - ] - } - ], - "report": { - "formatters": [ - { - "type": "text", - "layout": "table" - } - ], - "types": { - "APICoverage": { - "OpenAPI": { - "successCriteria": { - "minThresholdPercentage": 100, - "maxMissedEndpointsInSpec": 0, - "enforce": true - }, - "excludedEndpoints": [ - "/internal/metrics" - ] - } - } - } - }, - "security": { - "OpenAPI": { - "securitySchemes": { - "oAuth2AuthCode": { - "type": "oauth2", - "token": "OAUTH1234" - } - } - } - } -} diff --git a/specmatic.yaml b/specmatic.yaml new file mode 100644 index 0000000..13f01a2 --- /dev/null +++ b/specmatic.yaml @@ -0,0 +1,19 @@ +sources: + - provider: git + repository: https://github.com/znsio/specmatic-order-contracts.git + test: + - in/specmatic/examples/store/api_order_with_oauth_v3.yaml + +report: + formatters: + - type: text + layout: table + types: + APICoverage: + OpenAPI: + successCriteria: + minThresholdPercentage: 70 + maxMissedEndpointsInSpec: 4 + enforce: true + excludedEndpoints: + - /internal/metrics diff --git a/src/main/java/com/store/controllers/Orders.kt b/src/main/java/com/store/controllers/Orders.kt index c519269..92df794 100644 --- a/src/main/java/com/store/controllers/Orders.kt +++ b/src/main/java/com/store/controllers/Orders.kt @@ -49,7 +49,7 @@ class Orders { @GetMapping("/orders") fun search( - @RequestParam(name = "status", required = false) status: String?, + @RequestParam(name = "status", required = false) status: OrderStatus?, @RequestParam(name = "productid", required = false) productid: Int? ): List = orderService.findOrders(status, productid) } diff --git a/src/main/java/com/store/controllers/Products.kt b/src/main/java/com/store/controllers/Products.kt index 7f4ecd5..96e8b15 100644 --- a/src/main/java/com/store/controllers/Products.kt +++ b/src/main/java/com/store/controllers/Products.kt @@ -1,6 +1,7 @@ package com.store.controllers import com.store.exceptions.NotFoundException +import com.store.exceptions.ValidationException import com.store.model.Id import com.store.model.Product import com.store.model.User @@ -13,6 +14,8 @@ import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* import javax.validation.Valid +private val typesOfProducts = listOf("gadget", "book", "food", "other") + @RestController open class Products { @@ -26,6 +29,10 @@ open class Products { @Valid @RequestBody product: Product, @AuthenticationPrincipal user: User ): ResponseEntity { + productService.addProduct(product.also { + if(product.type !in typesOfProducts) + throw ValidationException("type must be one of ${typesOfProducts.joinToString(", ")}") + }) productService.updateProduct(product) return ResponseEntity(HttpStatus.OK) } @@ -41,7 +48,10 @@ open class Products { @PostMapping("/products") fun create(@Valid @RequestBody newProduct: Product, @AuthenticationPrincipal user: User): ResponseEntity { - val productId = productService.addProduct(newProduct) + val productId = productService.addProduct(newProduct.also { + if(newProduct.type !in typesOfProducts) + throw ValidationException("type must be one of ${typesOfProducts.joinToString(", ")}") + }) return ResponseEntity(productId, HttpStatus.OK) } diff --git a/src/main/java/com/store/exceptions/NotFoundException.kt b/src/main/java/com/store/exceptions/NotFoundException.kt index 73893d7..96091c1 100644 --- a/src/main/java/com/store/exceptions/NotFoundException.kt +++ b/src/main/java/com/store/exceptions/NotFoundException.kt @@ -4,4 +4,5 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(HttpStatus.NOT_FOUND) -class NotFoundException(validationErrorMessage: String = "") : RuntimeException(validationErrorMessage) \ No newline at end of file +class NotFoundException(private val validationErrorMessage: String = "") : RuntimeException(validationErrorMessage) { +} \ No newline at end of file diff --git a/src/main/java/com/store/exceptions/UnrecognizedTypeException.kt b/src/main/java/com/store/exceptions/UnrecognizedTypeException.kt new file mode 100644 index 0000000..edf93bd --- /dev/null +++ b/src/main/java/com/store/exceptions/UnrecognizedTypeException.kt @@ -0,0 +1,7 @@ +package com.store.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +@ResponseStatus(HttpStatus.BAD_REQUEST) +class UnrecognizedTypeException(type: String) : Throwable("Unrecognized type: $type") diff --git a/src/main/java/com/store/exceptions/ValidationException.kt b/src/main/java/com/store/exceptions/ValidationException.kt index 4818a38..3bec74c 100644 --- a/src/main/java/com/store/exceptions/ValidationException.kt +++ b/src/main/java/com/store/exceptions/ValidationException.kt @@ -4,4 +4,5 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(HttpStatus.BAD_REQUEST) -class ValidationException(validationErrorMessage: String = "") : RuntimeException(validationErrorMessage) \ No newline at end of file +class ValidationException(private val validationErrorMessage: String = "") : RuntimeException(validationErrorMessage) { +} \ No newline at end of file diff --git a/src/main/java/com/store/handlers/GlobalExceptionHandler.kt b/src/main/java/com/store/handlers/GlobalExceptionHandler.kt new file mode 100644 index 0000000..3a7b44c --- /dev/null +++ b/src/main/java/com/store/handlers/GlobalExceptionHandler.kt @@ -0,0 +1,59 @@ +package com.store.handlers + +import com.store.exceptions.NotFoundException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import java.time.LocalDateTime + +@ControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException::class) + fun handleGenericException(ex: NotFoundException): ResponseEntity { + val notFound = HttpStatus.NOT_FOUND + return ResponseEntity.status(notFound).body( + errorResponse( + notFound, + ex, + "Requested resource not found", + "resource not found" + ) + ) + } + + @ExceptionHandler(Exception::class) + fun handleGenericException(ex: Exception): ResponseEntity { + val badRequest = HttpStatus.BAD_REQUEST + return ResponseEntity.status(badRequest).body( + errorResponse( + badRequest, + ex, + "An error occurred while processing the request", + "Unknown error" + ) + ) + } + + private fun errorResponse( + httpStatus: HttpStatus, + ex: Exception, + error: String, + message: String + ): ErrorResponse { + return ErrorResponse( + LocalDateTime.now(), + httpStatus.value(), + error, + ex.message ?: message + ) + } +} + +data class ErrorResponse( + val timestamp: LocalDateTime, + val status: Int, + val error: String, + val message: String +) \ No newline at end of file diff --git a/src/main/java/com/store/model/DB.kt b/src/main/java/com/store/model/DB.kt index 70fce85..7758f61 100644 --- a/src/main/java/com/store/model/DB.kt +++ b/src/main/java/com/store/model/DB.kt @@ -1,9 +1,14 @@ package com.store.model +import com.store.exceptions.UnrecognizedTypeException +import javax.validation.ValidationException + object DB { - private var PRODUCTS: MutableMap = mutableMapOf(10 to Product("XYZ Phone", "gadget", 10, 10), 20 to Product("Gemini", "dog", 10, 20)) - private var ORDERS: MutableMap = mutableMapOf(10 to Order(10, 2, "pending", 10), 20 to Order(10, 1, "pending", 20)) - private val USERS: Map = mapOf("API-TOKEN-HARI" to User("Hari")) + private var PRODUCTS: MutableMap = + mutableMapOf(10 to Product("XYZ Phone", "gadget", 10, 10), 20 to Product("Gemini", "dog", 10, 20)) + private var ORDERS: MutableMap = + mutableMapOf(10 to Order(10, 2, OrderStatus.pending, 10), 20 to Order(10, 1, OrderStatus.pending, 20)) + private val USERS: Map = mapOf("API-TOKEN-SPEC" to User("Hari")) fun userCount(): Int { return USERS.values.count() @@ -11,7 +16,7 @@ object DB { fun resetDB() { PRODUCTS = mutableMapOf(10 to Product("XYZ Phone", "gadget", 10, 10), 20 to Product("Gemini", "dog", 10, 20)) - ORDERS = mutableMapOf(10 to Order(10, 2, "pending", 10), 20 to Order(10, 1, "pending", 20)) + ORDERS = mutableMapOf(10 to Order(10, 2, OrderStatus.pending, 10), 20 to Order(10, 1, OrderStatus.pending, 20)) } fun addProduct(product: Product) { @@ -27,9 +32,14 @@ object DB { } } - fun deleteProduct(id: Int) { PRODUCTS.remove(id) } + fun deleteProduct(id: Int) { + PRODUCTS.remove(id) + } fun findProducts(name: String?, type: String?, status: String?): List { + if (type != null && type !in listOf("book", "food", "gadget", "other")) + throw UnrecognizedTypeException(type) + return PRODUCTS.filter { (id, product) -> product.name == name || product.type == type || inventoryStatus(id) == status }.values.toList() @@ -52,7 +62,7 @@ object DB { ORDERS.remove(id) } - fun findOrders(status: String?, productId: Int?): List { + fun findOrders(status: OrderStatus?, productId: Int?): List { return ORDERS.filter { (_, order) -> order.status == status || order.productid == productId }.values.toList() @@ -68,6 +78,8 @@ object DB { } fun reserveProductInventory(productId: Int, count: Int) { + if (productId !in PRODUCTS) + throw ValidationException("Product Id $productId does not exist") val updatedProduct = PRODUCTS.getValue(productId).let { it.copy(inventory = it.inventory - count) } diff --git a/src/main/java/com/store/model/Order.kt b/src/main/java/com/store/model/Order.kt index 05f558d..5ac5da7 100644 --- a/src/main/java/com/store/model/Order.kt +++ b/src/main/java/com/store/model/Order.kt @@ -4,8 +4,14 @@ import java.util.concurrent.atomic.AtomicInteger import javax.validation.constraints.NotNull import javax.validation.constraints.Positive -class Order(@field:Positive val productid: Int = 0, @field:Positive val count: Int = 0, @field:NotNull var status: String = "pending", val id: Int = idGenerator.getAndIncrement()) { +class Order(@field:Positive val productid: Int = 0, @field:Positive val count: Int = 0, @field:NotNull var status: OrderStatus = OrderStatus.pending, val id: Int = idGenerator.getAndIncrement()) { companion object { val idGenerator: AtomicInteger = AtomicInteger() } } + +enum class OrderStatus { + pending, + fulfilled, + cancelled +} \ No newline at end of file diff --git a/src/main/java/com/store/model/Product.kt b/src/main/java/com/store/model/Product.kt index 7ac070c..9a6e7f3 100644 --- a/src/main/java/com/store/model/Product.kt +++ b/src/main/java/com/store/model/Product.kt @@ -8,10 +8,17 @@ import javax.validation.constraints.Positive data class Product( @field:NotNull @field:JsonDeserialize(using = StrictStringDeserializer::class) val name: String = "", @field:NotNull val type: String = "gadget", - @field:Positive val inventory: Int = 0, + @field:NotNull @field:Positive val inventory: Int = 0, val id: Int = idGenerator.getAndIncrement() ) { companion object { val idGenerator: AtomicInteger = AtomicInteger() } } + +enum class ProductType { + book, + food, + gadget, + other +} \ No newline at end of file diff --git a/src/main/java/com/store/services/OrderService.kt b/src/main/java/com/store/services/OrderService.kt index 6e8f206..6ba2620 100644 --- a/src/main/java/com/store/services/OrderService.kt +++ b/src/main/java/com/store/services/OrderService.kt @@ -4,6 +4,7 @@ import com.store.exceptions.ValidationException import com.store.model.DB import com.store.model.Id import com.store.model.Order +import com.store.model.OrderStatus import org.springframework.stereotype.Service @Service @@ -28,7 +29,7 @@ class OrderService { DB.updateOrder(order) } - fun findOrders(status: String?, productid: Int?): List { + fun findOrders(status: OrderStatus?, productid: Int?): List { return DB.findOrders(status, productid) } } \ No newline at end of file