mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-25 16:22:35 +03:00
edge-contracts: add rest-api contracts
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
dependencies {
|
||||
implementation(rootProject.libs.spring.cloud.stream)
|
||||
implementation(rootProject.libs.json.schema.validator)
|
||||
implementation(rootProject.libs.springBoot.starter.web)
|
||||
implementation(rootProject.libs.springData.commons)
|
||||
implementation(rootProject.libs.springDoc.openapi.starter)
|
||||
implementation(rootProject.libs.springCloud.stream)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.GetCustomerResponse
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import java.util.UUID
|
||||
|
||||
@RequestMapping(value = ["/api/customer"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
interface CustomerApi {
|
||||
|
||||
@GetMapping("/{guid}")
|
||||
@Throws(NotFoundException::class)
|
||||
fun getCustomer(@PathVariable guid: UUID): ResponseEntity<GetCustomerResponse>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
interface GreetingApi {
|
||||
|
||||
@GetMapping("/greeting")
|
||||
fun greet(): String
|
||||
|
||||
@GetMapping(value = ["/example/html"], produces = ["text/html"])
|
||||
fun exampleHtml(): String
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.GetNekoImagesResponse
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
|
||||
@RequestMapping(value = ["/api/neko"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
interface NekoApi {
|
||||
|
||||
@GetMapping("/categories")
|
||||
fun categories(): ResponseEntity<Set<String>>
|
||||
|
||||
@GetMapping("/images/{category}")
|
||||
fun images(
|
||||
@PathVariable category: String,
|
||||
@RequestParam imagesCount: Int = 1,
|
||||
): ResponseEntity<GetNekoImagesResponse>
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.UnprocessableException
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ProductApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.request.CreateProductRequest
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.BaseResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.NotFoundResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.page.PageResponse
|
||||
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
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import java.util.UUID
|
||||
|
||||
@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
interface ProductApi {
|
||||
|
||||
@GetMapping("")
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "200", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = PageResponse::class)),
|
||||
]),
|
||||
])
|
||||
fun getProducts(
|
||||
@ParameterObject pageable: Pageable,
|
||||
): ResponseEntity<PageResponse<ProductApiModel>>
|
||||
|
||||
@GetMapping("/{guid}")
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "200", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = ProductApiModel::class)),
|
||||
]),
|
||||
ApiResponse(responseCode = "404", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = NotFoundResponse::class))
|
||||
])
|
||||
])
|
||||
@Throws(NotFoundException::class)
|
||||
fun getProduct(
|
||||
@PathVariable guid: UUID,
|
||||
): ResponseEntity<ProductApiModel>
|
||||
|
||||
@PostMapping(value = [""], consumes = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun createProduct(
|
||||
@Valid @RequestBody product: CreateProductRequest,
|
||||
): ResponseEntity<ProductApiModel>
|
||||
|
||||
@DeleteMapping("/{guid}")
|
||||
@Throws(NotFoundException::class, UnprocessableException::class)
|
||||
fun deleteProduct(
|
||||
@PathVariable guid: UUID,
|
||||
): ResponseEntity<BaseResponse>
|
||||
|
||||
@PostMapping("/{guid}/send")
|
||||
@Throws(NotFoundException::class)
|
||||
fun sendProduct(
|
||||
@PathVariable guid: UUID,
|
||||
@RequestParam(required = false) topic: String?
|
||||
): ResponseEntity<BaseResponse>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.exceptions
|
||||
|
||||
open class ApiException(
|
||||
override val message: String,
|
||||
override val cause: Throwable?,
|
||||
) : RuntimeException(message, cause)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.exceptions
|
||||
|
||||
class NotFoundException(
|
||||
override val cause: Throwable?,
|
||||
): ApiException(
|
||||
message = "Not found",
|
||||
cause = cause,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.exceptions
|
||||
|
||||
class UnprocessableException(
|
||||
override val message: String,
|
||||
override val cause: Throwable? = null,
|
||||
): RuntimeException(message, cause)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.model
|
||||
|
||||
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
|
||||
import com.github.dannecron.demo.db.serialialization.UuidSerialization
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class CityApiModel(
|
||||
val id: Long,
|
||||
@Serializable(with = UuidSerialization::class)
|
||||
val guid: UUID,
|
||||
val name: String,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val createdAt: OffsetDateTime,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val updatedAt: OffsetDateTime?,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val deletedAt: OffsetDateTime?,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.model
|
||||
|
||||
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
|
||||
import com.github.dannecron.demo.db.serialialization.UuidSerialization
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class CustomerApiModel(
|
||||
val id: Long,
|
||||
@Serializable(with = UuidSerialization::class)
|
||||
val guid: UUID,
|
||||
val name: String,
|
||||
val cityId: Long?,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val createdAt: OffsetDateTime,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val updatedAt: OffsetDateTime?,
|
||||
)
|
||||
@@ -1,14 +1,13 @@
|
||||
package com.github.dannecron.demo.http.responses.neko
|
||||
package com.github.dannecron.demo.edgecontracts.api.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
data class NekoImageApiModel(
|
||||
val url: String,
|
||||
val animeName: String?,
|
||||
val artistHref: String?,
|
||||
val artistName: String?,
|
||||
val sourceUrl: String?,
|
||||
) {
|
||||
fun isGif() = animeName != null
|
||||
}
|
||||
val isGif: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.model
|
||||
|
||||
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
|
||||
import com.github.dannecron.demo.db.serialialization.UuidSerialization
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ProductApiModel(
|
||||
val id: Long,
|
||||
@Serializable(with = UuidSerialization::class)
|
||||
val guid: UUID,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val price: Long,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val createdAt: OffsetDateTime,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val updatedAt: OffsetDateTime?,
|
||||
@Serializable(with = OffsetDateTimeSerialization::class)
|
||||
val deletedAt: OffsetDateTime?,
|
||||
val priceDouble: Double,
|
||||
val isDeleted: Boolean,
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.github.dannecron.demo.http.responses
|
||||
package com.github.dannecron.demo.edgecontracts.api.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
|
||||
enum class ResponseStatus(@JsonValue val status: String) {
|
||||
enum class ResponseStatusModel(@JsonValue val status: String) {
|
||||
OK("ok"),
|
||||
NOT_FOUND("not found"),
|
||||
BAD_REQUEST("bad request"),
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.github.dannecron.demo.http.requests
|
||||
package com.github.dannecron.demo.edgecontracts.api.request
|
||||
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateProductRequest(
|
||||
@field:NotBlank(message = "name value required")
|
||||
val name: String,
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.CityApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.CustomerApiModel
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetCustomerResponse(
|
||||
val customer: CustomerApiModel,
|
||||
val city: CityApiModel?,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.NekoImageApiModel
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetNekoImagesResponse(
|
||||
val images: List<NekoImageApiModel>,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.common
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
|
||||
data class BadRequestResponse(
|
||||
val cause: String,
|
||||
): BaseResponse(status = ResponseStatusModel.BAD_REQUEST)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.common
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
|
||||
open class BaseResponse(val status: ResponseStatusModel)
|
||||
|
||||
fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatusModel.OK)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.common
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
|
||||
class NotFoundResponse: BaseResponse(ResponseStatusModel.NOT_FOUND)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.common
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
import org.springframework.validation.ObjectError
|
||||
|
||||
class UnprocessableResponse(
|
||||
val cause: String,
|
||||
val errors: List<ObjectError>?
|
||||
): BaseResponse(status = ResponseStatusModel.UNPROCESSABLE) {
|
||||
constructor(cause: String): this(cause, null)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.github.dannecron.demo.http.responses.page
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.page
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.github.dannecron.demo.http.responses.page
|
||||
package com.github.dannecron.demo.edgecontracts.api.response.page
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.data.domain.Page
|
||||
Reference in New Issue
Block a user