diff --git a/src/main/kotlin/com/example/demo/config/AppConfig.kt b/src/main/kotlin/com/example/demo/config/AppConfig.kt index a708e05..b59f6a1 100644 --- a/src/main/kotlin/com/example/demo/config/AppConfig.kt +++ b/src/main/kotlin/com/example/demo/config/AppConfig.kt @@ -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.kafka.Producer 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.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule @@ -53,7 +54,7 @@ class AppConfig( fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository) @Bean - fun schemaValidator(): SchemaValidator = SchemaValidator(kafkaProperties.validation.schema) + fun schemaValidator(): SchemaValidator = SchemaValidatorImp(kafkaProperties.validation.schema) @Bean fun otlpHttpSpanExporter(@Value("\${tracing.url}") url: String): OtlpHttpSpanExporter { diff --git a/src/main/kotlin/com/example/demo/services/validation/SchemaValidator.kt b/src/main/kotlin/com/example/demo/services/validation/SchemaValidator.kt index 6123616..21756e6 100644 --- a/src/main/kotlin/com/example/demo/services/validation/SchemaValidator.kt +++ b/src/main/kotlin/com/example/demo/services/validation/SchemaValidator.kt @@ -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.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 SchemaValidator( - private val schemaMap: Map, -) { - private val loadedSchema: MutableMap = mutableMapOf() - - fun validate(schemaName: String, value: JsonElement) { - - val schema = JsonSchema.fromDefinition( - getSchema(schemaName), - ) - - val errors = mutableListOf() - - 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 - } +interface SchemaValidator { + @Throws(ElementNotValidException::class, SchemaNotFoundException::class) + fun validate(schemaName: String, value: JsonElement) } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/services/validation/SchemaValidatorImp.kt b/src/main/kotlin/com/example/demo/services/validation/SchemaValidatorImp.kt new file mode 100644 index 0000000..930be8b --- /dev/null +++ b/src/main/kotlin/com/example/demo/services/validation/SchemaValidatorImp.kt @@ -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, +): SchemaValidator { + private val loadedSchema: MutableMap = mutableMapOf() + + override fun validate(schemaName: String, value: JsonElement) { + + val schema = JsonSchema.fromDefinition( + getSchema(schemaName), + ) + + val errors = mutableListOf() + + 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 + } +} \ No newline at end of file diff --git a/src/main/resources/json-schemas/product/sync.json b/src/main/resources/json-schemas/product/sync.json index 226d730..6b381b6 100644 --- a/src/main/resources/json-schemas/product/sync.json +++ b/src/main/resources/json-schemas/product/sync.json @@ -2,7 +2,7 @@ "schema": "http://json-schema.org/draft-07/schema#", "title": "event sync product", "type": "object", - "required": ["id", "guid"], + "required": ["id", "guid", "name", "price", "createdAt"], "properties": { "id": { "type": "number" @@ -19,8 +19,8 @@ { "type": "null" } ] }, - "number": { - "type": "string" + "price": { + "type": "number" }, "createdAt": { "type": "string" diff --git a/src/test/kotlin/com/example/demo/BaseUnitTest.kt b/src/test/kotlin/com/example/demo/BaseUnitTest.kt index c06d83d..a75f3bb 100644 --- a/src/test/kotlin/com/example/demo/BaseUnitTest.kt +++ b/src/test/kotlin/com/example/demo/BaseUnitTest.kt @@ -27,7 +27,7 @@ open class BaseUnitTest { autoOffsetReset = "none", ), validation = KafkaProperties.Validation( - schema = mapOf("product-sync" to "foo"), + schema = mapOf("product-sync" to "product/sync.json"), ), ) } diff --git a/src/test/kotlin/com/example/demo/services/validation/SchemaValidatorImpTest.kt b/src/test/kotlin/com/example/demo/services/validation/SchemaValidatorImpTest.kt new file mode 100644 index 0000000..d1c1b46 --- /dev/null +++ b/src/test/kotlin/com/example/demo/services/validation/SchemaValidatorImpTest.kt @@ -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?) { + 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, + ) + ) + } +} \ No newline at end of file