diff --git a/src/main/kotlin/com/github/erosb/jsonsKema/Format.kt b/src/main/kotlin/com/github/erosb/jsonsKema/Format.kt index 75a079e..93f3cff 100644 --- a/src/main/kotlin/com/github/erosb/jsonsKema/Format.kt +++ b/src/main/kotlin/com/github/erosb/jsonsKema/Format.kt @@ -175,7 +175,7 @@ internal val formatLoader: KeywordLoader = { ctx -> } data class FormatValidationFailure( override val schema: FormatSchema, - override val instance: IJsonValue + override val instance: IJsonValue, ) : ValidationFailure( message = "instance does not match format '${schema.format}'", keyword = Keyword.FORMAT, diff --git a/src/main/kotlin/com/github/erosb/jsonsKema/MinLength.kt b/src/main/kotlin/com/github/erosb/jsonsKema/MinLength.kt index ed3a068..964c031 100644 --- a/src/main/kotlin/com/github/erosb/jsonsKema/MinLength.kt +++ b/src/main/kotlin/com/github/erosb/jsonsKema/MinLength.kt @@ -11,7 +11,8 @@ internal val minLengthLoader: KeywordLoader = { ctx -> data class MinLengthValidationFailure( override val schema: MinLengthSchema, - override val instance: IJsonString + override val instance: IJsonString, + val dynamicPath: JsonPointer ) : ValidationFailure( "actual string length ${instance.value.length} is lower than minLength ${schema.minLength}", schema, diff --git a/src/main/kotlin/com/github/erosb/jsonsKema/Pattern.kt b/src/main/kotlin/com/github/erosb/jsonsKema/Pattern.kt index a8b05d7..b6a5f52 100644 --- a/src/main/kotlin/com/github/erosb/jsonsKema/Pattern.kt +++ b/src/main/kotlin/com/github/erosb/jsonsKema/Pattern.kt @@ -14,6 +14,7 @@ internal val patternLoader: KeywordLoader = { ctx -> data class PatternValidationFailure( override val schema: PatternSchema, override val instance: IJsonValue, + val dynamicPath: JsonPointer ) : ValidationFailure( message = "instance value did not match pattern ${schema.pattern}", schema = schema, diff --git a/src/main/kotlin/com/github/erosb/jsonsKema/Type.kt b/src/main/kotlin/com/github/erosb/jsonsKema/Type.kt index 41c28c6..eaa140e 100644 --- a/src/main/kotlin/com/github/erosb/jsonsKema/Type.kt +++ b/src/main/kotlin/com/github/erosb/jsonsKema/Type.kt @@ -27,7 +27,8 @@ data class TypeValidationFailure( data class MultiTypeValidationFailure( val actualInstanceType: String, override val schema: MultiTypeSchema, - override val instance: IJsonValue + override val instance: IJsonValue, + val dynamicPath: JsonPointer ) : ValidationFailure( "expected type: one of ${schema.types.elements.joinToString { ", " }}, actual: $actualInstanceType", schema, diff --git a/src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt b/src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt index 0cf7094..1f60490 100644 --- a/src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt +++ b/src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt @@ -207,17 +207,17 @@ private class DefaultValidator( inner class TypeValidatingVisitor(private val schema: TypeSchema) : AbstractTypeValidatingVisitor() { - override fun checkType(actualType: String): ValidationFailure? = inPathSegment(Keyword.TYPE) { + override fun checkType(actualType: String): ValidationFailure? { if (actualType == "integer" && schema.type.value == "number") { - return@inPathSegment null + return null } - return@inPathSegment if (schema.type.value == actualType) { + return if (schema.type.value == actualType) { null } else TypeValidationFailure( actualType, this.schema, instance, - dynamicPath() + dynamicPath() + Keyword.TYPE ) } } @@ -234,7 +234,8 @@ private class DefaultValidator( } else MultiTypeValidationFailure( actualType, this.schema, - instance + instance, + dynamicPath() + Keyword.TYPE ) } } @@ -297,7 +298,7 @@ private class DefaultValidator( return instance.maybeString { val length = it.value.codePointCount(0, it.value.length) if (length < schema.minLength) { - MinLengthValidationFailure(schema, it) + MinLengthValidationFailure(schema, it, dynamicPath() + Keyword.MIN_LENGTH) } else { null } @@ -336,9 +337,9 @@ private class DefaultValidator( failures.firstOrNull() } - override fun visitPatternSchema(schema: PatternSchema): ValidationFailure? { - return instance.maybeString { str -> - schema.pattern.patternMatchingFailure(str.value)?.let { PatternValidationFailure(schema, str) } + override fun visitPatternSchema(schema: PatternSchema): ValidationFailure? = instance.maybeString { str -> + schema.pattern.patternMatchingFailure(str.value)?.let { + PatternValidationFailure(schema, str, dynamicPath() + Keyword.PATTERN) } } @@ -605,7 +606,7 @@ private class DefaultValidator( } override fun visitAllOfSchema(schema: AllOfSchema): ValidationFailure? = inPathSegment(Keyword.ALL_OF) { - val subFailures = schema.subschemas.map { subschema -> subschema.accept(this) }.filterNotNull() + val subFailures = collectSubschemaFailures(schema) if (subFailures.isNotEmpty()) { AllOfValidationFailure(schema = schema, instance = instance, causes = subFailures.toSet(), dynamicPath()) } else { @@ -613,8 +614,13 @@ private class DefaultValidator( } } + private fun collectSubschemaFailures(schema: Schema) = + schema.subschemas().mapIndexed { index, subschema -> + inPathSegment(index.toString()) { subschema.accept(this) } + }.filterNotNull() + override fun visitAnyOfSchema(schema: AnyOfSchema): ValidationFailure? = inPathSegment(Keyword.ANY_OF) { - val subFailures = schema.subschemas.map { subschema -> subschema.accept(this) }.filterNotNull() + val subFailures = collectSubschemaFailures(schema) if (subFailures.size == schema.subschemas.size) { AnyOfValidationFailure(schema = schema, instance = instance, causes = subFailures.toSet(), dynamicPath()) } else { @@ -623,7 +629,7 @@ private class DefaultValidator( } override fun visitOneOfSchema(schema: OneOfSchema): ValidationFailure? = inPathSegment(Keyword.ONE_OF) { - val subFailures = schema.subschemas.map { subschema -> subschema.accept(this) }.filterNotNull() + val subFailures = collectSubschemaFailures(schema) if ((schema.subschemas.size - subFailures.size) == 1) { null } else { diff --git a/src/test/kotlin/com/github/erosb/jsonsKema/ApplicatorValidationTest.kt b/src/test/kotlin/com/github/erosb/jsonsKema/ApplicatorValidationTest.kt index 089cdc9..8d4bb74 100644 --- a/src/test/kotlin/com/github/erosb/jsonsKema/ApplicatorValidationTest.kt +++ b/src/test/kotlin/com/github/erosb/jsonsKema/ApplicatorValidationTest.kt @@ -29,10 +29,10 @@ class ApplicatorValidationTest { println(allOfFailure) assertEquals("#/allOf", allOfFailure.dynamicPath.toString()) val anyOfFailure = allOfFailure.causes.single() as AnyOfValidationFailure - assertEquals("#/allOf/anyOf", anyOfFailure.dynamicPath.toString()) + assertEquals("#/allOf/0/anyOf", anyOfFailure.dynamicPath.toString()) val oneOfFailure = anyOfFailure.causes.single() as OneOfValidationFailure - assertEquals("#/allOf/anyOf/oneOf", oneOfFailure.dynamicPath.toString()) + assertEquals("#/allOf/0/anyOf/0/oneOf", oneOfFailure.dynamicPath.toString()) val notFailure = oneOfFailure.causes.single() as NotValidationFailure - assertEquals("#/allOf/anyOf/oneOf/not", notFailure.dynamicPath.toString()) + assertEquals("#/allOf/0/anyOf/0/oneOf/0/not", notFailure.dynamicPath.toString()) } } diff --git a/src/test/kotlin/com/github/erosb/jsonsKema/StringValidationTest.kt b/src/test/kotlin/com/github/erosb/jsonsKema/StringValidationTest.kt new file mode 100644 index 0000000..9d396a8 --- /dev/null +++ b/src/test/kotlin/com/github/erosb/jsonsKema/StringValidationTest.kt @@ -0,0 +1,43 @@ +package com.github.erosb.jsonsKema + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class StringValidationTest { + + @Test + fun test() { + val schema = SchemaLoader(JsonParser(""" + { + "allOf": [ + { "type": "string" }, + { "$ref": "#/$defs/Details" } + ], + "$defs": { + "Details": { + "minLength": 5, + "maxLength": 3, + "pattern": "^a*$", + "format": "email" + } + } + } + """.trimIndent())())() + + val actual = Validator.forSchema(schema).validate(JsonString("bbbb", UnknownSource))!! + + println(actual) + val detailsFailure = actual.causes.single() + val minLengthFailure = detailsFailure.causes.filterIsInstance().single() + assertEquals("#/allOf/1/$ref/minLength", minLengthFailure.dynamicPath.toString()) + + val maxLengthFailure = detailsFailure.causes.filterIsInstance().single() + assertEquals("#/allOf/1/$ref/maxLength", maxLengthFailure.dynamicPath.toString()) + + val patternFailure = detailsFailure.causes.filterIsInstance().single() + assertEquals("#/allOf/1/$ref/pattern", patternFailure.dynamicPath.toString()) + +// val emailFailure = detailsFailure.causes.filterIsInstance().single() +// assertEquals("#/allOf/1/$ref/pattern", emailFailure.dynamicPath.toString()) + } +} diff --git a/src/test/kotlin/com/github/erosb/jsonsKema/TypeValidationTest.kt b/src/test/kotlin/com/github/erosb/jsonsKema/TypeValidationTest.kt index b4c7b18..2c64d49 100644 --- a/src/test/kotlin/com/github/erosb/jsonsKema/TypeValidationTest.kt +++ b/src/test/kotlin/com/github/erosb/jsonsKema/TypeValidationTest.kt @@ -2,6 +2,7 @@ package com.github.erosb.jsonsKema import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -61,7 +62,7 @@ class TypeValidationTest { )() val actual = Validator.forSchema(SchemaLoader(schema)()).validate(instance) as TypeValidationFailure - Assertions.assertEquals( + assertEquals( JsonParser( """ { @@ -76,4 +77,15 @@ class TypeValidationTest { ) assertEquals(JsonPointer("type"), actual.dynamicPath) } + + @Test + fun multiTypeDynamicPathTest() { + val schema = SchemaLoader(JsonParser(""" + { "type": [ "null", "integer" ]} + """.trimIndent())())() + + val actual = Validator.forSchema(schema).validate(JsonNumber(2.2, UnknownSource)) as MultiTypeValidationFailure + + assertEquals("#/type", actual.dynamicPath.toString()) + } }