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