Skip to content

Commit

Permalink
improves dynamicPath of applicator failures by including indexes, als…
Browse files Browse the repository at this point in the history
…o adds minLength/maxLength/pattern validationr failure dynamic paths
  • Loading branch information
erosb committed Dec 8, 2024
1 parent 5976ce1 commit 4fd81a9
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/com/github/erosb/jsonsKema/Format.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/github/erosb/jsonsKema/MinLength.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/github/erosb/jsonsKema/Pattern.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/github/erosb/jsonsKema/Type.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 18 additions & 12 deletions src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand All @@ -234,7 +234,8 @@ private class DefaultValidator(
} else MultiTypeValidationFailure(
actualType,
this.schema,
instance
instance,
dynamicPath() + Keyword.TYPE
)
}
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -605,16 +606,21 @@ 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 {
null
}
}

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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
43 changes: 43 additions & 0 deletions src/test/kotlin/com/github/erosb/jsonsKema/StringValidationTest.kt
Original file line number Diff line number Diff line change
@@ -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<MinLengthValidationFailure>().single()
assertEquals("#/allOf/1/$ref/minLength", minLengthFailure.dynamicPath.toString())

val maxLengthFailure = detailsFailure.causes.filterIsInstance<MaxLengthValidationFailure>().single()
assertEquals("#/allOf/1/$ref/maxLength", maxLengthFailure.dynamicPath.toString())

val patternFailure = detailsFailure.causes.filterIsInstance<PatternValidationFailure>().single()
assertEquals("#/allOf/1/$ref/pattern", patternFailure.dynamicPath.toString())

// val emailFailure = detailsFailure.causes.filterIsInstance<FormatValidationFailure>().single()
// assertEquals("#/allOf/1/$ref/pattern", emailFailure.dynamicPath.toString())
}
}
14 changes: 13 additions & 1 deletion src/test/kotlin/com/github/erosb/jsonsKema/TypeValidationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,7 +62,7 @@ class TypeValidationTest {
)()
val actual = Validator.forSchema(SchemaLoader(schema)()).validate(instance) as TypeValidationFailure

Assertions.assertEquals(
assertEquals(
JsonParser(
"""
{
Expand All @@ -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())
}
}

0 comments on commit 4fd81a9

Please sign in to comment.