move schema validation to separate package

This commit is contained in:
Savosin Denis
2025-06-03 11:20:27 +07:00
parent 31e57678d4
commit fd73d94d70
10 changed files with 22 additions and 18 deletions

View File

@@ -0,0 +1,4 @@
dependencies {
implementation(rootProject.libs.spring.cloud.stream)
implementation(rootProject.libs.json.schema.validator)
}

View File

@@ -0,0 +1,11 @@
package com.github.dannecron.demo.edgecontracts.validation
import com.github.dannecron.demo.edgecontracts.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.edgecontracts.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.JsonElement
interface SchemaValidator {
@Throws(ElementNotValidException::class, SchemaNotFoundException::class)
fun validate(schemaName: String, value: JsonElement)
}

View File

@@ -0,0 +1,32 @@
package com.github.dannecron.demo.edgecontracts.validation
import com.github.dannecron.demo.edgecontracts.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.edgecontracts.validation.exceptions.SchemaNotFoundException
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.ValidationError
import kotlinx.serialization.json.JsonElement
class SchemaValidatorImp(
private val schemaMap: Map<String, String>,
): SchemaValidator {
@Throws(
SchemaNotFoundException::class,
ElementNotValidException::class,
)
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)
}
}
}
@Throws(SchemaNotFoundException::class)
private fun getSchema(schemaName: String) = schemaMap[schemaName]
?: throw SchemaNotFoundException()
}

View File

@@ -0,0 +1,21 @@
package com.github.dannecron.demo.edgecontracts.validation.config
import com.github.dannecron.demo.edgecontracts.validation.SchemaValidator
import com.github.dannecron.demo.edgecontracts.validation.SchemaValidatorImp
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.util.ResourceUtils
@Configuration
class SchemaValidationConfig(
private val validationProperties: ValidationProperties,
) {
@Bean
fun schemaValidator(): SchemaValidator = SchemaValidatorImp(
schemaMap = validationProperties.schema.mapValues {
schema -> ResourceUtils.getFile("classpath:json-schemas/${schema.value}")
.readText(Charsets.UTF_8)
}
)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
{
"schema": "http://json-schema.org/draft-07/schema#",
"title": "event sync product",
"type": "object",
"required": ["id", "guid", "name", "price", "createdAt"],
"properties": {
"id": {
"type": "number"
},
"guid": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
]
},
"price": {
"type": "number"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
]
},
"deletedAt": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
]
}
}
}

View File

@@ -0,0 +1,99 @@
package com.github.dannecron.demo.edgecontracts.validation
import com.github.dannecron.demo.edgecontracts.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.edgecontracts.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 kotlin.reflect.KClass
import kotlin.test.assertFailsWith
class SchemaValidatorImpTest {
private val schemaValidatorImp = SchemaValidatorImp(
schemaMap = mapOf(
KAFKA_PRODUCT_SYNC_SCHEMA to getJsonSchema("json-schemas/kafka/product/sync.json")),
)
@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)
return
}
assertFailsWith(expectedException) {
schemaValidatorImp.validate(schemaName = schemaName, value = element)
}
}
companion object {
private const val KAFKA_PRODUCT_SYNC_SCHEMA = "kafkaProductSync"
@JvmStatic
fun validateDataProvider() = listOf(
Arguments.of(
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"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
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"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
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"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,
)
)
}
@Suppress("SameParameterValue")
private fun getJsonSchema(resourcePath: String) = javaClass.classLoader
.getResourceAsStream(resourcePath)!!
.readAllBytes()
.toString(Charsets.UTF_8)
}