move validation to separate core package, add generation service

some code cleanup
This commit is contained in:
Savosin Denis
2025-05-05 12:54:17 +07:00
parent d7c051746d
commit 7c12208883
42 changed files with 669 additions and 713 deletions

View File

@@ -1,8 +0,0 @@
package com.github.dannecron.demo.config.properties
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("validation")
data class ValidationProperties(
val schema: Map<String, String>
)

View File

@@ -1,31 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table("city")
@Serializable
data class City(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun isDeleted(): Boolean = deletedAt != null
}

View File

@@ -1,27 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table("customer")
@Serializable
data class Customer(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val cityId: Long?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
)

View File

@@ -1,5 +1,7 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.db.entity.Customer
import kotlinx.serialization.Serializable
@Serializable

View File

@@ -1,6 +1,7 @@
package com.github.dannecron.demo.models.order
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.entity.order.Order
data class OrderWithProducts(
val order: Order,

View File

@@ -1,37 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import com.github.dannecron.demo.utils.roundTo
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "product")
@Serializable
data class Product(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val description: String?,
val price: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2)
fun isDeleted(): Boolean = deletedAt != null
}

View File

@@ -1,31 +0,0 @@
package com.github.dannecron.demo.models.order
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "order")
@Serializable
data class Order(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val customerId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "delivered_at")
val deliveredAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?
) {
fun isDelivered(): Boolean = deliveredAt != null
}

View File

@@ -1,41 +0,0 @@
package com.github.dannecron.demo.models.order
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.Transient
import org.springframework.data.domain.Persistable
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "order_product")
@Serializable
data class OrderProduct(
@Id
@Serializable(with = UuidSerialization::class)
val guid: UUID,
@Column(value = "order_id")
val orderId: Long,
@Column(value = "product_id")
val productId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
): Persistable<UUID> {
@Transient
var isNewInstance: Boolean? = null
override fun getId(): UUID {
return guid
}
override fun isNew(): Boolean {
return isNewInstance ?: true
}
}

View File

@@ -1,19 +0,0 @@
package com.github.dannecron.demo.models.serializables
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
object OffsetDateTimeSerialization: KSerializer<OffsetDateTime> {
override val descriptor = PrimitiveSerialDescriptor("Time", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): OffsetDateTime = OffsetDateTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
encoder.encodeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

View File

@@ -1,18 +0,0 @@
package com.github.dannecron.demo.models.serializables
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
object UuidSerialization: KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}

View File

@@ -1,6 +1,6 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto

View File

@@ -1,24 +1,28 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.providers.CityRepository
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.db.repository.CityRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
import org.springframework.stereotype.Service
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.UUID
@Service
class CityServiceImpl(
private val cityRepository: CityRepository
private val cityRepository: CityRepository,
private val commonGenerator: CommonGenerator,
): CityService {
override fun findByGuid(guid: UUID): City? = cityRepository.findByGuid(guid)
override fun create(name: String): City = City(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
deletedAt = null,
).let {

View File

@@ -1,6 +1,6 @@
package com.github.dannecron.demo.services.database.customer
import com.github.dannecron.demo.models.Customer
import com.github.dannecron.demo.db.entity.Customer
import com.github.dannecron.demo.models.CustomerExtended
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import java.util.*

View File

@@ -1,16 +1,19 @@
package com.github.dannecron.demo.services.database.customer
import com.github.dannecron.demo.models.Customer
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.Customer
import com.github.dannecron.demo.db.repository.CityRepository
import com.github.dannecron.demo.db.repository.CustomerRepository
import com.github.dannecron.demo.models.CustomerExtended
import com.github.dannecron.demo.providers.CityRepository
import com.github.dannecron.demo.providers.CustomerRepository
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import java.time.OffsetDateTime
import java.util.*
import org.springframework.stereotype.Service
import java.util.UUID
@Service
class CustomerServiceImpl(
private val customerRepository: CustomerRepository,
private val cityRepository: CityRepository
private val cityRepository: CityRepository,
private val commonGenerator: CommonGenerator,
): CustomerService {
override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid)
?.let {
@@ -22,12 +25,12 @@ class CustomerServiceImpl(
override fun create(name: String, cityGuid: UUID?): Customer = Customer(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
cityId = cityGuid?.let {
cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException()
},
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
).let {
customerRepository.save(it)

View File

@@ -1,55 +1,65 @@
package com.github.dannecron.demo.services.database.order
import com.github.dannecron.demo.models.Customer
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.models.order.Order
import com.github.dannecron.demo.models.order.OrderProduct
import com.github.dannecron.demo.models.order.OrderWithProducts
import com.github.dannecron.demo.providers.OrderProductRepository
import com.github.dannecron.demo.providers.OrderRepository
import com.github.dannecron.demo.providers.ProductRepository
import org.springframework.beans.factory.annotation.Autowired
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.Customer
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.entity.order.Order
import com.github.dannecron.demo.db.entity.order.OrderProduct
import com.github.dannecron.demo.db.repository.OrderProductRepository
import com.github.dannecron.demo.db.repository.OrderRepository
import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.models.OrderWithProducts
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.OffsetDateTime
import java.util.*
@Service
class OrderServiceImpl(
@Autowired private val orderRepository: OrderRepository,
@Autowired private val orderProductRepository: OrderProductRepository,
@Autowired private val productRepository: ProductRepository,
private val orderRepository: OrderRepository,
private val orderProductRepository: OrderProductRepository,
private val productRepository: ProductRepository,
private val commonGenerator: CommonGenerator,
) {
fun getByCustomerId(customerId: Long): List<OrderWithProducts> = orderRepository.findByCustomerId(customerId)
fun findByCustomerId(customerId: Long): List<OrderWithProducts> = orderRepository.findByCustomerId(customerId)
.let { orders -> orders.map { order -> OrderWithProducts(
order = order,
products = orderProductRepository.findByOrderId(orderId = order.id!!)
.map { orderProduct -> orderProduct.productId }
.let { productIds -> productRepository.findAllById(productIds).toList() }
products = findProductsByOrderId(order.id!!),
) } }
@Transactional
fun createOrder(customer: Customer, products: Set<Product>): Order {
val order = Order(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
customerId = customer.id!!,
deliveredAt = null,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
)
return orderRepository.save(order)
.also {
savedOrder -> products.toList()
.map { product -> OrderProduct(
guid = UUID.randomUUID(),
orderId = savedOrder.id!!,
productId = product.id!!,
createdAt = OffsetDateTime.now(),
updatedAt = null
) }
.also { orderProductRepository.saveAll(it) }
.also { saveProductsForNewOrder(it, products.toList()) }
}
private fun findProductsByOrderId(orderId: Long): List<Product> =
orderProductRepository.findByOrderId(orderId = orderId)
.map { it.productId }
.let {
if (it.isEmpty()) {
emptyList()
} else {
productRepository.findAllById(it).toList()
}
}
private fun saveProductsForNewOrder(savedOrder: Order, products: List<Product>) {
products.map {
OrderProduct(
guid = commonGenerator.generateUUID(),
orderId = savedOrder.id!!,
productId = it.id!!,
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null
)
}.also { orderProductRepository.saveAll(it) }
}
}

View File

@@ -1,13 +1,13 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.util.*
import java.util.UUID
@Service
interface ProductService {

View File

@@ -1,21 +1,26 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.providers.ProductRepository
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
import com.github.dannecron.demo.services.kafka.Producer
import com.github.dannecron.demo.services.kafka.dto.ProductDto
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
import com.github.dannecron.demo.utils.LoggerDelegate
import net.logstash.logback.marker.Markers
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import java.time.OffsetDateTime
import java.util.*
import org.springframework.stereotype.Service
import java.time.format.DateTimeFormatter
import java.util.UUID
@Service
class ProductServiceImpl(
private val defaultSyncTopic: String,
private val productRepository: ProductRepository,
private val producer: Producer,
private val commonGenerator: CommonGenerator,
): ProductService {
private val logger by LoggerDelegate()
@@ -32,11 +37,11 @@ class ProductServiceImpl(
override fun create(name: String, price: Long, description: String?): Product {
val product = Product(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
description = description,
price = price,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
deletedAt = null,
)
@@ -52,7 +57,7 @@ class ProductServiceImpl(
}
val deletedProduct = product.copy(
deletedAt = OffsetDateTime.now(),
deletedAt = commonGenerator.generateCurrentTime(),
)
return productRepository.save(deletedProduct)
@@ -61,7 +66,17 @@ class ProductServiceImpl(
override fun syncToKafka(guid: UUID, topic: String?) {
val product = findByGuid(guid) ?: throw ProductNotFoundException()
producer.produceProductInfo(topic ?: defaultSyncTopic, product)
producer.produceProductSync(product.toKafkaDto())
}
private fun Product.toKafkaDto() = ProductDto(
id = id ?: throw InvalidArgumentException("product.id"),
guid = guid.toString(),
name = name,
description = description,
price = price,
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
updatedAt = updatedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
deletedAt = deletedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
)
}

View File

@@ -1,14 +0,0 @@
package com.github.dannecron.demo.services.validation
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.JsonElement
interface SchemaValidator {
companion object {
const val SCHEMA_KAFKA_PRODUCT_SYNC = "kafka-product-sync"
}
@Throws(ElementNotValidException::class, SchemaNotFoundException::class)
fun validate(schemaName: String, value: JsonElement)
}

View File

@@ -1,42 +0,0 @@
package com.github.dannecron.demo.services.validation
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.ValidationError
import kotlinx.serialization.json.JsonElement
import org.springframework.util.ResourceUtils
class SchemaValidatorImp(
private val schemaMap: Map<String, String>,
): SchemaValidator {
private val loadedSchema: MutableMap<String, String> = mutableMapOf()
override fun validate(schemaName: String, value: JsonElement) {
JsonSchema.fromDefinition(
getSchema(schemaName),
).also {
val errors = mutableListOf<ValidationError>()
if (!it.validate(value, errors::add)) {
throw ElementNotValidException(errors)
}
}
}
private fun getSchema(schemaName: String): String {
val loaded = loadedSchema[schemaName]
if (loaded != null) {
return loaded
}
val schemaFile = schemaMap[schemaName]
?: throw SchemaNotFoundException()
val schema = ResourceUtils.getFile("classpath:json-schemas/$schemaFile")
.readText(Charsets.UTF_8)
loadedSchema[schemaName] = schema
return schema
}
}

View File

@@ -1,7 +0,0 @@
package com.github.dannecron.demo.services.validation.exceptions
import io.github.optimumcode.json.schema.ValidationError
class ElementNotValidException(
val validationErrors: List<ValidationError>,
): RuntimeException()

View File

@@ -1,3 +0,0 @@
package com.github.dannecron.demo.services.validation.exceptions
class SchemaNotFoundException: RuntimeException()

View File

@@ -1,14 +0,0 @@
package com.github.dannecron.demo.utils
import kotlin.math.pow
import kotlin.math.roundToInt
fun Double.roundTo(numFractionDigits: Int): Double {
val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor
}
fun String.snakeToCamelCase(): String {
val pattern = "_[a-z]".toRegex()
return replace(pattern) { it.value.last().uppercase() }
}