refactor validator, add tests

This commit is contained in:
Denis Savosin
2024-10-11 13:16:59 +07:00
parent ddff3675e8
commit 7cd50456a0
6 changed files with 150 additions and 42 deletions

View File

@@ -10,6 +10,7 @@ import com.example.demo.services.database.product.ProductService
import com.example.demo.services.database.product.ProductServiceImpl import com.example.demo.services.database.product.ProductServiceImpl
import com.example.demo.services.kafka.Producer import com.example.demo.services.kafka.Producer
import com.example.demo.services.validation.SchemaValidator import com.example.demo.services.validation.SchemaValidator
import com.example.demo.services.validation.SchemaValidatorImp
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
@@ -53,7 +54,7 @@ class AppConfig(
fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository) fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository)
@Bean @Bean
fun schemaValidator(): SchemaValidator = SchemaValidator(kafkaProperties.validation.schema) fun schemaValidator(): SchemaValidator = SchemaValidatorImp(kafkaProperties.validation.schema)
@Bean @Bean
fun otlpHttpSpanExporter(@Value("\${tracing.url}") url: String): OtlpHttpSpanExporter { fun otlpHttpSpanExporter(@Value("\${tracing.url}") url: String): OtlpHttpSpanExporter {

View File

@@ -2,43 +2,9 @@ package com.example.demo.services.validation
import com.example.demo.services.validation.exceptions.ElementNotValidException import com.example.demo.services.validation.exceptions.ElementNotValidException
import com.example.demo.services.validation.exceptions.SchemaNotFoundException import com.example.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 kotlinx.serialization.json.JsonElement
import org.springframework.util.ResourceUtils
class SchemaValidator( interface SchemaValidator {
private val schemaMap: Map<String, String>, @Throws(ElementNotValidException::class, SchemaNotFoundException::class)
) { fun validate(schemaName: String, value: JsonElement)
private val loadedSchema: MutableMap<String, String> = mutableMapOf()
fun validate(schemaName: String, value: JsonElement) {
val schema = JsonSchema.fromDefinition(
getSchema(schemaName),
)
val errors = mutableListOf<ValidationError>()
val valid = schema.validate(value, errors::add)
if (!valid) {
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

@@ -0,0 +1,44 @@
package com.example.demo.services.validation
import com.example.demo.services.validation.exceptions.ElementNotValidException
import com.example.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) {
val schema = JsonSchema.fromDefinition(
getSchema(schemaName),
)
val errors = mutableListOf<ValidationError>()
val valid = schema.validate(value, errors::add)
if (!valid) {
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

@@ -2,7 +2,7 @@
"schema": "http://json-schema.org/draft-07/schema#", "schema": "http://json-schema.org/draft-07/schema#",
"title": "event sync product", "title": "event sync product",
"type": "object", "type": "object",
"required": ["id", "guid"], "required": ["id", "guid", "name", "price", "createdAt"],
"properties": { "properties": {
"id": { "id": {
"type": "number" "type": "number"
@@ -19,8 +19,8 @@
{ "type": "null" } { "type": "null" }
] ]
}, },
"number": { "price": {
"type": "string" "type": "number"
}, },
"createdAt": { "createdAt": {
"type": "string" "type": "string"

View File

@@ -27,7 +27,7 @@ open class BaseUnitTest {
autoOffsetReset = "none", autoOffsetReset = "none",
), ),
validation = KafkaProperties.Validation( validation = KafkaProperties.Validation(
schema = mapOf("product-sync" to "foo"), schema = mapOf("product-sync" to "product/sync.json"),
), ),
) )
} }

View File

@@ -0,0 +1,97 @@
package com.example.demo.services.validation
import com.example.demo.BaseUnitTest
import com.example.demo.services.validation.exceptions.ElementNotValidException
import com.example.demo.services.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.Json
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import kotlin.reflect.KClass
import kotlin.test.assertFailsWith
@RunWith(SpringRunner::class)
@SpringBootTest
class SchemaValidatorImpTest(
@Autowired val schemaValidatorImp: SchemaValidatorImp
): BaseUnitTest() {
@ParameterizedTest
@MethodSource("validateDataProvider")
fun validate(schemaName: String, inputRawJson: String, expectedException: KClass<out Throwable>?) {
val element = Json.parseToJsonElement(inputRawJson.trimIndent())
if (expectedException == null) {
schemaValidatorImp.validate(schemaName = schemaName, value = element)
// second time should use cache
schemaValidatorImp.validate(schemaName = schemaName, value = element)
return
}
assertFailsWith(expectedException) {
schemaValidatorImp.validate(schemaName = schemaName, value = element)
}
}
companion object {
@JvmStatic
fun validateDataProvider() = listOf(
Arguments.of(
"product-sync",
"""
{
"id": 123,
"guid": "3a27e322-b5b6-427f-b761-a02284c1cfa4",
"name": "some-name",
"description": null,
"price": 12.22,
"createdAt": "2024-01-01T12:11:10+04:00",
"updatedAt": null,
"deletedAt": null
}
""",
null,
),
Arguments.of( // no id
"product-sync",
"""
{
"guid": "3a27e322-b5b6-427f-b761-a02284c1cfa4",
"name": "some-name",
"description": null,
"price": 12.22,
"createdAt": "2024-01-01T12:11:10+04:00",
"updatedAt": null,
"deletedAt": null
}
""",
ElementNotValidException::class,
),
Arguments.of( // wrong guid
"product-sync",
"""
{
"id": 213,
"guid": 77373,
"name": "some-name",
"description": null,
"price": 12.22,
"createdAt": "2024-01-01T12:11:10+04:00",
"updatedAt": null,
"deletedAt": null
}
""",
ElementNotValidException::class,
),
Arguments.of(
"some-unknown-schema",
"{}",
SchemaNotFoundException::class,
)
)
}
}